297 lines
6.7 KiB
Markdown
297 lines
6.7 KiB
Markdown
|
|
# 第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 <domain name or dotted-decimal>\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 <host> <port>\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)
|