Files
obsidian/操作系统/09_网络编程基础/09_网络编程基础.md

6.7 KiB
Raw Blame History

第09讲网络编程

🎯 本节目标:掌握 Socket 编程,理解客户端-服务器模型

📋 前置知识


🤔 为什么需要这个?

你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过:

  • 浏览器是怎么从服务器获取网页的?
  • 两台电脑之间是怎么通信的?

网络编程就是让你能够编写这样的程序。

生活比喻

  • Socket = 电话插座
  • 服务器 = 客服中心(等待来电)
  • 客户端 = 拨打电话的用户
  • 端口 = 分机号

📖 核心概念

1. 客户端-服务器模型

sequenceDiagram
    participant 客户端
    participant 服务器

    服务器->>服务器: socket() 创建套接字
    服务器->>服务器: bind() 绑定地址
    服务器->>服务器: listen() 监听连接
    服务器->>服务器: accept() 等待连接

    客户端->>客户端: socket() 创建套接字
    客户端->>服务器: connect() 发起连接

    服务器->>客户端: 连接建立

    客户端->>服务器: send() 发送数据
    服务器->>客户端: recv() 接收数据
    服务器->>客户端: send() 发送响应
    客户端->>服务器: recv() 接收响应

    客户端->>客户端: close() 关闭连接
    服务器->>服务器: close() 关闭连接

2. Socket 编程流程

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 地址与端口

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 存储多字节数据的方式不同:

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

转换函数

htonl()  // host to network long
htons()  // host to network short
ntohl()  // network to host long
ntohs()  // network to host short

5. DNS 域名解析

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查询主机信息

// 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);
}

编译运行

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

示例2TCP 服务器(大小写转换)

// 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);
    }
}

示例3TCP 客户端

// 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);
}

编译运行

# 编译
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    # 输出(大小写转换)

🔗 知识关联


📝 思考题

  1. TCP vs UDP:什么时候用 TCP什么时候用 UDP
  2. 为什么需要字节序转换? 如果不转换会怎样?
  3. 服务器为什么需要 bind() 客户端为什么不需要?

📚 扩展阅读