# 第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)
