# 第09讲:网络编程 > 🎯 **本节目标**:掌握 Socket 编程,理解客户端-服务器模型 ## 📋 前置知识 - [[04_文件IO编程]] — 文件描述符的概念 - [[06_进程控制]] — 进程创建 --- ## 🤔 为什么需要这个? 你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过: - 浏览器是怎么从服务器获取网页的? - 两台电脑之间是怎么通信的? **网络编程**就是让你能够编写这样的程序。 **生活比喻**: - **Socket** = 电话插座 - **服务器** = 客服中心(等待来电) - **客户端** = 拨打电话的用户 - **端口** = 分机号 --- ## 📖 核心概念 ### 1. 客户端-服务器模型 ```mermaid sequenceDiagram participant 客户端 participant 服务器 服务器->>服务器: socket() 创建套接字 服务器->>服务器: bind() 绑定地址 服务器->>服务器: listen() 监听连接 服务器->>服务器: accept() 等待连接 客户端->>客户端: socket() 创建套接字 客户端->>服务器: connect() 发起连接 服务器->>客户端: 连接建立 客户端->>服务器: send() 发送数据 服务器->>客户端: recv() 接收数据 服务器->>客户端: send() 发送响应 客户端->>服务器: recv() 接收响应 客户端->>客户端: close() 关闭连接 服务器->>服务器: close() 关闭连接 ``` ### 2. Socket 编程流程 ```mermaid graph TD subgraph 服务器端 A1[socket] --> A2[bind] A2 --> A3[listen] A3 --> A4[accept] A4 --> A5[read/write] A5 --> A6[close] end subgraph 客户端 B1[socket] --> B2[connect] B2 --> B3[write/read] B3 --> B4[close] end style A1 fill:#e8f5e9 style B1 fill:#e1f5fe ``` **核心函数**: | 函数 | 作用 | 服务器/客户端 | |------|------|:---:| | `socket()` | 创建套接字 | 都需要 | | `bind()` | 绑定地址和端口 | 服务器 | | `listen()` | 开始监听 | 服务器 | | `accept()` | 接受连接 | 服务器 | | `connect()` | 发起连接 | 客户端 | | `send()` | 发送数据 | 都需要 | | `recv()` | 接收数据 | 都需要 | | `close()` | 关闭连接 | 都需要 | ### 3. IP 地址与端口 ```mermaid graph LR A[计算机] -->|IP地址| B[192.168.1.100] A -->|端口| C[:80] B --> D[唯一标识一台电脑] C --> E[唯一标识一个服务] style B fill:#e1f5fe style C fill:#e8f5e9 ``` **常见端口**: | 端口 | 服务 | 说明 | |:---:|------|------| | 22 | SSH | 远程登录 | | 80 | HTTP | 网页浏览 | | 443 | HTTPS | 安全网页 | | 3306 | MySQL | 数据库 | | 8080 | HTTP备用 | 开发常用 | ### 4. 字节序 不同 CPU 存储多字节数据的方式不同: ```mermaid graph LR A[0x12345678] --> B[大端序 Big-endian] A --> C[小端序 Little-endian] B --> B1[12 34 56 78] C --> C1[78 56 34 12] style B fill:#e1f5fe style C fill:#e8f5e9 ``` **网络字节序**:大端序(Big-endian) **转换函数**: ```c htonl() // host to network long htons() // host to network short ntohl() // network to host long ntohs() // network to host short ``` ### 5. DNS 域名解析 ```mermaid graph LR A[www.example.com] -->|DNS查询| B[DNS服务器] B -->|返回IP| C[93.184.216.34] C -->|连接| D[Web服务器] style A fill:#e1f5fe style C fill:#e8f5e9 ``` --- ## 💻 动手实践 ### 示例1:查询主机信息 ```c // hostinfo.c - 查询主机信息 #include "wrapper.h" int main(int argc, char **argv) { char **pp; struct in_addr addr; struct hostent *hostp; if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); exit(0); } // 判断是域名还是IP地址 if (inet_aton(argv[1], &addr) != 0) hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET); else hostp = Gethostbyname(argv[1]); // 打印主机信息 printf("official hostname: %s\n", hostp->h_name); for (pp = hostp->h_aliases; *pp != NULL; pp++) printf("alias: %s\n", *pp); for (pp = hostp->h_addr_list; *pp != NULL; pp++) { addr.s_addr = *((unsigned int *)*pp); printf("address: %s\n", inet_ntoa(addr)); } exit(0); } ``` **编译运行**: ```bash gcc -o hostinfo hostinfo.c -L. -lwrapper ./hostinfo www.baidu.com ``` **预期输出**: ``` official hostname: www.a.shifen.com alias: www.baidu.com address: 110.242.68.66 address: 110.242.68.3 ``` ### 示例2:TCP 服务器(大小写转换) ```c // toggle.c - TCP 服务器 #include "wrapper.h" void toggle(int conn_sock) { size_t n; int i; char buf[MAXLINE]; while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) { printf("toggle server received %d bytes\n", n); // 转换大小写 for (i = 0; i < n; i++) if (isupper(buf[i])) buf[i] = tolower(buf[i]); else if (islower(buf[i])) buf[i] = toupper(buf[i]); send(conn_sock, buf, n, 0); } } ``` ### 示例3:TCP 客户端 ```c // togglec.c - TCP 客户端 #include "wrapper.h" int main(int argc, char **argv) { int client_sock, port; char *host, buf[MAXLINE]; rio_t rio; if (argc != 3) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); } host = argv[1]; port = atoi(argv[2]); // 连接服务器 client_sock = open_client_sock(host, port); // 从标准输入读取,发送到服务器,接收响应 while (fgets(buf, MAXLINE, stdin) != NULL) { send(client_sock, buf, strlen(buf), 0); recv(client_sock, buf, MAXLINE, 0); fputs(buf, stdout); } close(client_sock); exit(0); } ``` **编译运行**: ```bash # 编译 gcc -o toggle toggle.c -L. -lwrapper gcc -o togglec togglec.c -L. -lwrapper # 终端1:启动服务器 ./toggle 8080 # 终端2:启动客户端 ./togglec localhost 8080 Hello World # 输入 HELLO WORLD # 输出(大小写转换) ``` --- ## 🔗 知识关联 - Socket 是文件描述符,详见 [[04_文件IO编程]] - 并发服务器在 [[10_并发服务器]] 中有详细讲解 - 网络编程在 [[实验05_Linux网络通信编程]] 中有实践练习 --- ## 📝 思考题 1. **TCP vs UDP**:什么时候用 TCP,什么时候用 UDP? 2. **为什么需要字节序转换?** 如果不转换会怎样? 3. **服务器为什么需要 bind()?** 客户端为什么不需要? --- ## 📚 扩展阅读 - 《UNIX网络编程》第1卷:套接字联网API - [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) - [Socket 编程详解](https://www.cs.rpi.edu/~moorthy/Courses/os98/Pggrams/socket.html)