vault backup: 2026-06-13 23:46:22
This commit is contained in:
296
操作系统/09_网络编程基础/09_网络编程基础.md
Normal file
296
操作系统/09_网络编程基础/09_网络编程基础.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# 第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)
|
||||
Reference in New Issue
Block a user