linux网络编程

  • 网络编程是指编写运行在多个网络节点上的程序,实现网络通信的程序。
  • 网络编程的目的是实现网络通信,网络通信需要通过网络协议进行数据交换。
  • 网络编程的实现需要考虑网络通信的效率、可靠性、安全性等问题。

  • osi七层模型

https://pan.lmio.xyz/pic/16a643b30cee87d9f2b6a16157f5ae8a.png

  • 上述OSI的七层网络模型是一种理想模型,在实际应用中效率较低,在实际应用中主要使用TCP/IP四层协议栈:

https://pan.lmio.xyz/pic/50a1f22a2632268419f4d83a04428ccb.png

TCP/IP 四层模型是现行普适的网络层次模型。由顶层到底层是:

  • 应用层:包含 OSI 模型中的最顶部三层,加密、压缩、编码、会话、语义等等功能统统是应用程序 APP 所辖范畴。 应用程序 APP 可以很简单,也可以很复杂。
  • 传输层(位与操作系统内核):与 OSI 模型对应,负责传输控制。
  • 网络层(位与操作系统内核):与 OSI 模型对应,负责路由选择。
  • 网络接口与物理层(位与操作系统内核):这包括实际数据传输的物理媒介,及其对应的驱动层软件。
  • 网络协议是通信计算机双方必须共同遵从的一组约定。

  • 网络协议包括:通信协议、数据协议、传输协议、网络管理协议、安全协议等。

  • 网络协议是通信计算机双方必须共同遵守的规则,包括:

    1. 起始与终止
    2. 传输控制
    3. 传输协议
    4. 错误处理
    5. 流量控制
    6. 数据处理

数据包在网络中传输时,发送方逐层对数据包进行对应协议的封装,这个过程类似于寄包裹的时候往包裹上贴标签。而接收方执行相反的过程,接收方拆包裹并读取对应协议信息(一般称为协议头)。

https://pan.lmio.xyz/pic/f7e98c3a6080ccb80ed4329ade2b49da.png

层次 协议
应用层 HTTP, HTTPS, FTP, SMTP, SNMP, DNS, TFTP, …
传输层 TCP, UDP, …
网络层 IP, ICMP, ARP, RARP, …
数据链路层 SLIP, CSLIP, PPP, …
  • 套接字(socket)是网络通信中进程间进行双向通信的端点。
  • 套接字是通信的基石,是支持 TCP/IP 协议的网络通信的基本操作单元。
  • 它允许位于不同计算机上的进程进行双向通信。
  • 套接字是支持 TCP/IP 协议的网络通信的基本操作单元,也是支持各种应用层协议的网络通信的基本操作单元,因此套接字也称为“网络套接字”。
  • IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
  • 互联网上的主机和路由器使用IP地址进行通信。
  • IP地址的格式:IP地址通常被写成“点分十进制”的形式,例如:192.168.0.1。
  • 点分十进制形式中,每个点之间的数字代表该IP地址的子网掩码。
  • 子网掩码:子网掩码用来屏蔽IP地址的“网络部分”,从而得到该IP地址的“主机部分”。
  • 子网掩码是一个32位的二进制数,它由连续的1和连续的0组成。
  • 子网掩码的格式为“点分十进制”形式,例如:255.255.255.0

IP地址与子网掩码相与之后的结果被称为网段:

1
网段 = IP & 子网掩码

上述例子中,子网掩码是 255.255.255.0,因此网段就是 192.168.9.x,局域网内通信的主机应该都要处于同一网段内,否则数据无法被该网段所属的网关(192.168.9.1)路由转发。

在很多场合下,子网掩码会以比特位的形式跟IP地址写在一起,例如上述子网属性可以写成:

1
192.168.9.88/24

后面的 /24 代表该IP地址所在的网段是前24比特位,也就是说子网掩码是 255.255.255.0

IP 地址可以唯一标识一台计算机,但通信的双方并不是两台计算机,而是计算机内部的进程。很明显,为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来加以区分。

端口号是一个短整型数据,长度为16位。具体分布:

  • 系统端口:1~1023
  • 注册端口:1024~49150
  • 动态或私有端口:49151~65535(这是平常做实验可用的端口号范围)

TCP/UDP常见端口

当一个数据需要使用2个或以上字节来存储时,就会出现所谓字节序的概念。例如一个四字节数据,既可以将低有效位(78)放在低地址处,反之亦可放在高地址处。

1
int a = 0x12345678;

https://pan.lmio.xyz/pic/ba52de37253030566bebdd4ee750bc41.png

通常,将最低有效位(即78)放在低地址的存储方式称为小端序,反之即大端序。在单机编程中,字节序是系统内部的存储细节,与程序无关。但在网络编程中,由于数据是在两台不同的机器中传输和表达,因此如果字节序不一致,则会出现牛头不对马嘴的现象。

解决这一困境的办法是,将网络中的数据,统一为某种固定的字节序,比如大端序。这样一来,凡是从主机往外发的数据,一律转换为大端序,凡是从网络接收的数据,也一律是大端序,网络字节序屏蔽了通信双方的具体细节,从而使得双方通信成为可能。

https://pan.lmio.xyz/pic/c5d031b255e6e99ebf68c3a62a946c07.png

  • 客户端
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define HOST "127.0.0.1"
#define PORT 8080

int main() {
    // 新建socket 文件
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
    perror("socket");
        exit(-1);
    }

    // 设置服务器地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;                      // 设置为IPv4
    server_addr.sin_port = htons(PORT);                    // 转换为大端序
    server_addr.sin_addr.s_addr = inet_addr(HOST);  // 设置为本地地址

    // 连接服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect fail");
        exit(-1);
    }

    // 发送数据
    char msg[1024];
    while (1) {
        printf("请输入要发送的消息:\n");
        scanf("%s", msg);
        send(sockfd, msg, sizeof(msg), 0);
    }
}
  • 服务端
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include "threadpool.h"

#define MIN_THREADS 10
#define MAX_THREADS 128
#define HOST "127.0.0.1"
#define PORT 8080

void *socket_routine(void *arg) {
    int client_fd = *(int *)arg;

    // 接收客户端数据
    char buf[1024];
    while (1) {
        ssize_t recv_len = recv(client_fd, buf, sizeof(buf), 0);
        if (recv_len < 0) {
            perror("recv failed");
            close(client_fd);
            return NULL;
        }
        printf("Received message: %s\n", buf);
    }
}

int main() {
    // 创建socket 文件
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("create socket failed");
        exit(-1);
    }

    // 设置服务器地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 绑定socket 文件到服务器地址
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind addr failed");
        exit(-1);
    }

    // 监听socket 文件
    if (listen(sockfd, 10) < 0) {
        perror("socket listen failed");
        exit(-1);
    }

    // 接受客户端连接
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建线程池
    ThreadPool_t *pool = threadpool_init(MIN_THREADS, MAX_THREADS);
    while (1) {
        int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
        if (client_fd < 0) {
            perror("accept error");
            close(sockfd);
            exit(-1);
        }
        threadpool_add_task(pool, socket_routine, &client_fd);
    }

    // 关闭socket 文件
    close(sockfd);
    return 0;
}