网络协议编程

网络基础

协议

一组规则

分层模型结构

OSI七层模型:物理层 -> 数据链路层 -> 网络层 -> 传输层 -> 会话层 -> 表示层 -> 应用层
TCP/IP 网络模型: 网络接口层 -> 网络层 -> 传输层 -> 应用层

网络传输流程

端对端通信: 逐层封装,逐层解封

网络应用程序设计模式

2种常见的模型

C/S B/S
优点 缓存大量数据、协议选择灵活 安全性好、跨平台、开发工作量小
速度快
缺点 安全性差、不跨平台、开发工作量大 不能缓存大量数据、严格遵循http

socket编程

套接字概念

在TCP/IP协议中,IP地址+TCP或 UDP端口号标识网络通讯中的一个进程。
IP address + PORT 就可以对应一个soket.
欲建立的连接的2个进程各自有一个socket来标识,那么这个两个socket组成的socket pair (IP PORT IP PORT)唯一标识一个连接.
因此可以用Socket来描述网络连接一对一的关系。

tips:

  1. 在网络通信中,套接字一定是成对出现的,且具备唯一性 ,其中一端的发送缓冲区对应另一端的接收缓冲区。
  2. 一个描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)

关于socket缓存区[^1] :

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是 TCP 协议负责的事情。

网络字节序

由于历史遗留问题:intel架构的cpu采用小端存储,而IBM采用大端存储

TCP/IP协议规定: 网络数据流应采用大端字节序

字节序 描述 例子 内存(低->高)
小端 高位存放高地址,低位存放低地址 int a = 0x12345678 0x78563412
大端 低位存在高地址,高位存放低地址 int a = 0x12345678 0x12345678

又大多数PC本地存储都采用小端法,网络存储需要大端,故需要进行大小端转化。
在C语言中,为了解决这个问题使用网络程序具备可移植性,提供了以下库函数实现网络字节序到主机字节序的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <arpa/inet.h>
// h means host
// n means network
// l means long
// s means short

/** host -> network long(IP)*/
uint32_t htonl(uint32_t hostlong);
/** host -> network short(PORT)*/
uint16_t htons(uint16_t hostshort);

/** network -> host long(IP)*/
uint32_t ntohl(uint32_t netlong);
/** network -> host short(PORT)*/
uint16_t ntohs(uint16_t netshort);

IP地址转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <arpa/inet.h>
/**
@af: AF_INET、AF_INET6
@src: 传入IP地址(点分十进制)
@dst: 传出传换后的网络字节序的IP地址

return:
sucessful: 1
exception: 0, src is not vaild IP address
error: -1
*/
int inet_pton(int af, const char *src, void *dst); //本地字节序 -> 网络字节序

/**
@af: AF_INET、AF_INET6
@src: 传入网络字节序的IP地址
@dst: 传出IP地址(点分十进制)

return:
sucessful: dst
error: NULL
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); //网络字节序 -> 本地字节序

套接字地址结构

socketaddr结构与socketaddr_in的关系

sockaddr地址结构:

1
2
3
4
5
6
#include <arpa/inet.h>
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons(8080);
// method 1
int dst;
inet_pton(AF_INET, "127.0.0.1", (void *)&dst);
addr.sin_addr = dst;
// method 2 *
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 取出系统中有效的任意IP地址。二进制类型。INADDR_ANY -> 0
// method 3 **
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 指定具体的IP地址
bind(fd, (struct sockaddr *)&addr, size);

socket模型创建流程

socket函数

创建一个socket套接字

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>         
#include <sys/socket.h>

/**
@domain: AF_INET、AF_INET6、AF_UNIX
@type: SOCK_STREAM、SOCK_DGRAM
@protocol: 0 (会基于@type和@domain自动判断是tcp还是udp)

return:
sucessful: 新套接字对应文件描述符
fail: -1 error
*/
int socket(int domain, int type, int protocol);

bind函数

将一个套接字地址结构与现有的socket文件描述符绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/**
@sockfd: socket函数 返回值
@addr: 传入一个套接字地址结构指针
@addrlen: 地址结构大小

return:
sucessful: 0
fail: -1 errno
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数

设置同时与服务器建立连接的上限数,即同时进行3次握手的客户端数量

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/**
@sockfd: socket函数 返回值
@backlog: 上限数值。最大128.

return:
sucessful: 0
fail: -1 errno
*/
int listen(int sockfd, int backlog);

accept函数

阻塞等待客户端建立连接,成功时返回一个与客户端成功建立连接的socket文件描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/**
@sockfd: socket函数 返回值
@addr: 传出参数,成功与服务器建立连接的那个客户端的地址结构(IP+PORT)
@addrlen: 传入传出参数 入:addr的大小的socketlen_t指针。出:客户端addr实际大小

return:
sucessful: 能与服务器进行通信的socket对应的文件描述符
fail: -1, errno
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

connect函数

使用现有的socket与服务器建立连接

tips: 如果不使用bind绑定客户端地址结构,将采用“隐式绑定”.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/**
@sockfd: socket函数 返回值
@addr: 传入参数。服务器的地址结构
@addrlen: 服务器地址结构的大小

return:
sucessful: 0
fail: -1 errno
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

简单的数据双向传输(C/S)

实现利用socket使通信双方都能收发数据[^2]
服务端

接受请求部分被放在 while 循环中,服务端接受客户端数据,并原样返回.

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
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main() {

//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
int reuse = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)) == -1) {
printf("error!%s", strerror(errno));
return -1;
}

if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
printf("error!%s", strerror(errno));
return -1;
}

//进入监听状态,等待用户发起请求
listen(serv_sock, 20);

//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
char buffer[BUFSIZ] = {0};

while (1) {
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
int strLen = read(clnt_sock, buffer, BUFSIZ); //接收客户端发来的数据
//向客户端发送数据
write(clnt_sock, buffer, strLen);
//关闭套接字
close(clnt_sock);
memset(buffer, 0, BUFSIZ); //重置缓冲区
}

close(serv_sock);

return 0;
}

客户端

客户端中需要将 socket 创建与连接等操作放在 while 循环内部。因为服务器中调用 close(clnt_sock);不仅会关闭服务器端的 clnt_sock,还会通知客户端连接已断开,客户端也会清理 socket 相关资源,所以每次请求完毕都会清理 socket,下次发起请求时都需要重新创建。

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
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int main() {

//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口

char bufSend[BUFSIZ] = {0};
char bufRecv[BUFSIZ] = {0};

while (1) {
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
return -1;
//获取用户输入的字符串并发送给服务器
printf("Input a string: ");
fgets(bufSend, BUFSIZ, stdin);

send(sock, bufSend, strlen(bufSend), 0);
//接收服务器传回的数据
recv(sock, bufRecv, BUFSIZ, 0);
//输出接收到的数据
printf("Message form server: %s\n", bufRecv);

memset(bufSend, 0, BUFSIZ); //重置缓冲区
memset(bufRecv, 0, BUFSIZ); //重置缓冲区

close(sock);
}

return 0;
}

多进程并发服务器

创建步骤

  1. 创建监听套接字 lfd

    1
    socket();
  2. 绑定地址结构

    1
    bind();
  3. 监听客户端的请求

    1
    listen();
  4. 处理客户端的请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    while(1) {
    cfd = accept(); // 接受客户端的连结请求
    pid = fork();

    if(pid == 0) { // 子进程
    close(lfd);
    read();
    小 -> 大
    write();
    } else if (pid > 0){ // 父进程
    close(cfd); // 关闭用于与客户端通信的套接字 cfd
    continue;
    }
    }
  5. 父子进程

父进程:

1
2
3
4
5
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中,完成子进程回收
while(waitpid());

子进程:

1
2
3
4
close(lfd);
read();
小 -> 大
write();

多线程并发服务器

创建步骤

  1. 创建监听套接字 lfd
    1
    socket();
  2. 绑定地址结构
    1
    bind();
  3. 监听客户端的请求
    1
    listen();
  4. 处理客户端的请求
    1
    2
    3
    4
    5
    while(1) {
    cfd = accept(lfd,); // 接受客户端的连结请求
    pthread_create(&tid, NULL, tfn, NULL);
    pthread_detach(tid);
    }
  5. 子线程
    1
    2
    3
    4
    5
    6
    7
    void *tfn(void *arg)
    {
    close(lfd);
    read(cfd);
    小 -> 大
    write(cfd);
    }

RERFERENCE

[^1]: socket缓冲区. https://archlinuxstudio.github.io/LinuxNetworkProgrammingAndEncryption/#/network/socket?id=socket-%e7%bc%93%e5%86%b2%e5%8c%ba
[^2]: 简单的数据双向传输. https://archlinuxstudio.github.io/LinuxNetworkProgrammingAndEncryption/#/network/socket?id=%e5%8f%8c%e5%90%91%e6%95%b0%e6%8d%ae%e4%bc%a0%e8%be%93