计算机网络C++实践:试试远程进程通信

网络编程

网络编程是什么?有什么用?如何实现?

写在前面

本篇仅作简单介绍,内容适合入门级别

文中相关api就不做过多介绍,不然字数就太多了,可自行去api手册了解

语言不保证严谨,内容也不保证完全,欢迎指正错误、补充

本文我已经尽量减少字数了,还是1w多,不知道打开会不会卡 : )

一、网络编程基础

一、为什么引入网络编程

学完多进程、进程间通信,我们发现这些通信方式都是在同一个主机上多个进程之间进行的,并不能实现跨主机通信。如果想要实现跨主机的通信方式,就要用到socket套接字通信方式,也就是本篇讲的内容。

二、网络基础

都涉及到网络了,肯定是需要一定的计算机网络知识,这里我只简单提及

  • 协议:庞大的计算机网络中,要实现有条不紊的通信,就得按照一定的约定来进行数据的交换,这些规则、约定、标准统称为协议(protocol)

    • TCP:提供了面向连接的、可靠的数据传输服务
    • UCP:提供了面向无连接的、不保证数据可靠传输的、尽最大努力的传输协议
    • IP:网络层的一种协议,上面两种协议都会使用该协议
  • 网络体系结构

    • TCP/IP体系结构:应用层、传输层、网络层、链路层
    • 其中TCP、UDP协议在传输层,ip协议在网络层
  • 数据如何传输(封包拆包)

    • 用户数据在每一层加上包装,直到物理传输媒介,到达目的地后再不断拆包装得到用户数据

    • 就像下面这样

      用户数据
      FTP头 用户数据
      TCP头 FTP头 用户数据
      IP头 TCP头 FTP头 用户数据
      以太网头 IP头 TCP头 FTP头 用户数据
  • TCP与UDP区别

    • TCP提供了面向连接的、可靠的数据传输服务,每次传输需要双方应答,所以效率较低。
    • TCP通信中为了提高传输效率,会将多个较小的,发送时间间隔较短的数据包,沾成一个数据包进行发送,该现象称为沾包现象。解决方法就是拉长时间间隔,增大数据包大小,规定包的格式
    • UDP提供面向无连接、不保证数据可靠传输、尽最大努力传输的协议,不需要双方应答,所以效率较高
    • UDP通信中并不会发生沾包现象,适用于发送小尺寸,接收方不用或不方便应答的情况,如:广播、组播
  • 字节序

    • 计算机存储多字节整数时,根据CPU架构会分为大端存储和小端存储

    • 大端存储:内存地址低位存储数据的高位

    • 小端存储:内存地址地位存储数据的低位

    • 对于0x12345678,大端存储就是0x12 0x34 0x56 0x78;小端存储则是0x78 0x56 0x34 0x12

    • 不同主机的存储方式不同,为了统一,引入网络字节序的概念,规定网络字节序统一使用大端存储。那么数据传输就是统一转换成大端存储再转换成目标主机字节序格式即可

    • 几个转换字节序的api

      1
      2
      3
      4
      5
      6
      #include <arpa/inet.h>

      uint32_t htonl(uint32_t hostlong);//将4字节整数主机字节序转换为网络字节序,参数是主机字节序,返回值是网络字节序
      uint16_t htons(uint16_t hostshort);//将2字节整数主机字节序转换为网络字节序,参数是主机字节序,返回值是网络字节序
      uint32_t ntohl(uint32_t netlong);//将4字节整数的网络字节序转换为主机字节序,参数是网络字节序,返回值是主机字节序
      uint16_t ntohs(uint16_t netshort);//将2字节整数的网络字节序转换为主机字节序,参数是网络字节序,返回值是主机字节序
    • 多字节整数在网络传输时,需要字节序转换,单字节和字符串不需要

  • ip地址

    • 是主机在网络中的唯一标识,由网络号和主机号两部分组成,便于找到主机

    • 网络号:确定计算机从属的网络

    • 主机号:标识该设备在该网络中的编号

    • 分为IPv4和IPv6两种

    • IPv4:使用4字节无符号整数标识一个ip地址[0, 2^32-1],有四十多亿个

    • IPv6:使用16字节无符号整数标识一个ip地址[0, 2^128-1],非常多了

    • 五类网络:A、B、C、D、E

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      A类网络:1.0.0.0 -- 127.255.255.255		:已经保留不供给使用
      前一个字节是网络号开头固定为0,后面三个字节是主机号
      B类网络:128.0.0.0--191.255.255.255 :网络管理中心
      前两个字节是网络号开头固定为10,后面两个字节是主机号
      C类网络:192.0.0.0--223.255.255.255 :家庭、学校、公司使用
      前三个字节是网络号开头固定为110,后面一个字节是主机号
      D类网络:224.0.0.0--239.255.255.255 :组播IP
      四个字节都是网络号开头固定为1110,无主机号
      E类网络:240.0.0.0--255.255.255.255 :保留、实验室使用
      四个字节都是网络号开头固定为1111,无主机号
    • 点分十进制

      为了方便记忆,我们将ip地址每一个字节计算成十进制并用点分割,是一个字符串。但是其本质仍是四字节无符号整数,所以在网络中传输时,将点分十进制转化成四字节无符号整数进行传输。相关api如下:

      1
      2
      3
      4
      5
      6
      7
      8
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>

      in_addr_t inet_addr(const char *cp);
      //将点分十进制的ip地址转换为4字节无符号整数的网络字节序,参数为点分十进制数据,返回值时4字节无符号整数
      char *inet_ntoa(struct in_addr in);
      //将4字节无符号整数的网络字节序,转换为点分十进制的字符串
  • 端口号(port)

    • 端口号用于区分一个主机上的不同应用程序
    • 为什么不用进程号来区分?进程号是进程的唯一标识,同一个应用程序关闭再打开后不是同一个进程号了
    • 0 ~ 1023:VIP端口号,被特殊应用程序占用了
    • 1024 ~ 49151:用户可分配的端口号
    • 49152 ~ 65535:动态分配或系统自动分配的端口号

三、网络编程基础

一、套接字

  • 是网络通信的信息载体,其中有两个缓冲区:发送缓冲区、接收缓冲区

  • 原理是:将数据发送进套接字,然后套接字再将数据经过网络发送到对方的套接字,之后对方从套接字接收数据

  • socket函数

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

    int socket(int domain, int type, int protocol);
    功能:为通信创建一个端点,并返回该端点对应的文件描述符,
    参数1:协议族,常用的协议族如下

    参数2:通信类型,指定通信语义,常用的通信类型如下
    SOCK_STREAM 支持TCP面向连接的通信协议
    SOCK_DGRAM 支持UDP面向无连接的通信协议

    参数3:通信协议,当参数2中明确指定特定协议时,参数3可以设置为0,但是有多个协议共同使用时,需要用参数3指定当前套接字确定的协议
    返回值:成功返回创建的端点对应的文件描述符,失败返回-1并置位错误码

二、基于TCP面向连接的通信

网络通信方式有两种:基于BS模型(浏览器服务器模型)、基于CS模型(客户端服务器模型),本篇主要介绍CS模型,BS模型是http服务器用到的。

  • 原理(几个api)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    服务器端:
    1.socket()创建套接字用于连接
    2.bind()为套接字绑定IP和端口号,方便连接
    3.listen()将第一步创建的套接字设置成被动监听状态并在其中创建两个队列(半连接队列和已连接队列),因为socket创建的套接字只能主动连接
    4.accept()处理已连接队列中已建立的连接,并创建一个新的套接字用于和客户端套接字通信
    5.send()/recv()进行数据传输
    6.close()关闭套接字

    客户端
    1.socket()创建套接字用于连接
    2.bind()为套接字绑定IP和端口号,方便连接(客户端可选是否手动绑定,不手动绑定时,系统会自动分配)
    3.connect()将套接字连接到给定的地址上
    4.send()/recv()进行数据传输
    5.close()关闭套接字
  • TCP服务器端和客户端实现

    服务器端:

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
74
75
76
77
78
79
80
81
#include <myhead.h>  // 自定义头文件
#define SER_PORT 8888 // 服务器端口号(1024以上,避免VIP端口权限问题)
#define SER_IP "0.0.0.0" // 监听所有可用网卡IP(INADDR_ANY的点分十进制形式)

int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字
// domain:AF_INET(IPv4协议族);type:SOCK_STREAM(TCP流式套接字);protocol:0(默认使用TCP协议)
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1) // 错误判断:创建失败返回-1
{
perror("socket error"); // 打印错误信息(如权限不足、协议不支持等)
return -1;
}

// 2. 绑定IP和端口号(服务器必须绑定,否则客户端无法定位)
struct sockaddr_in sin; // 存储IPv4地址信息的结构体
sin.sin_family = AF_INET; // 协议族:必须与socket创建时一致
sin.sin_port = htons(SER_PORT); // 端口号:转换为网络字节序(大端)
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址:转换为网络字节序(0.0.0.0表示监听所有网卡)

// 绑定操作:将套接字与地址信息关联
if(bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error"); // 常见错误:端口被占用、IP不可用
return -1;
}
printf("bind success\n");

// 3. 启动监听(将主动套接字转为被动套接字)
// 第二个参数:已完成连接队列的最大长度(半连接队列长度由系统决定)
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4. 阻塞等待客户端连接(从已连接队列中取出连接)
struct sockaddr_in cin; // 存储客户端地址信息的结构体
socklen_t socklen = sizeof(cin); // 地址结构体长度(传入传出参数)
// 注意:accept返回新的套接字(newfd),用于与该客户端通信,原sfd继续监听新连接
int newfd = accept(sfd, (struct sockaddr*)&cin, &socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
// 打印客户端信息:inet_ntoa转换IP为点分十进制,ntohs转换端口为本地字节序
printf("[%s:%d]:连接成功!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

// 5. 数据收发(循环处理)
char rbuf[128] = ""; // 接收缓冲区
while(1)
{
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区(避免残留旧数据)
// 接收数据:从newfd读取客户端发送的数据,返回值为实际接收字节数
int res = recv(newfd, rbuf, sizeof(rbuf)-1, 0); // 留1字节存'\0'
if(res == 0) // 返回0表示客户端正常关闭连接
{
printf("对端已下线\n");
break;
}
printf("收到消息:%s\n", rbuf);

// 拼接响应数据并发送
strcat(rbuf, " :)");
// 发送数据:将处理后的数据发回客户端
if(send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}

// 6. 关闭套接字(释放资源)
close(newfd); // 关闭与客户端通信的套接字
close(sfd); // 关闭监听套接字
return 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
74
#include <myhead.h>
#define SER_PORT 8888 // 服务器端口号(需与服务器一致)
#define SER_IP "127.0.0.1" // 服务器IP(本地测试用回环地址)
// 客户端端口和IP可省略绑定(系统自动分配),此处仅为示例
#define CLI_PORT 9999
#define CLI_IP "127.0.0.1"

int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字(与服务器端一致)
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}

// 2. 绑定客户端地址(可选:系统会自动分配端口和IP)
struct sockaddr_in cin;
cin.sin_family = AF_INET;
cin.sin_port = htons(CLI_PORT); // 客户端端口(可选)
cin.sin_addr.s_addr = inet_addr(CLI_IP); // 客户端IP(可选)
if(bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
{
perror("bind error"); // 客户端绑定失败不影响连接(可注释此段)
return -1;
}
printf("bind success\n");

// 3. 连接服务器
struct sockaddr_in sin; // 服务器地址信息
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT); // 服务器端口(网络字节序)
sin.sin_addr.s_addr = inet_addr(SER_IP); // 服务器IP(网络字节序)
// 发起连接:阻塞直到连接建立或失败
if(connect(cfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("connect error"); // 常见错误:服务器未启动、IP/端口错误
return -1;
}
printf("连接服务器成功\n");

// 4. 数据收发
char wbuf[128] = ""; // 发送缓冲区
while(1)
{
bzero(wbuf, sizeof(wbuf));
// 从标准输入获取数据(用户输入)
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0'; // 去除fgets自带的换行符

// 发送数据到服务器
if(send(cfd, wbuf, strlen(wbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");

// 接收服务器响应
bzero(wbuf, sizeof(wbuf));
int res = recv(cfd, wbuf, sizeof(wbuf)-1, 0);
if(res == 0)
{
printf("服务器已下线\n");
break;
}
printf("收到服务器消息:%s\n", wbuf);
}

// 5. 关闭套接字
close(cfd);
return 0;
}

三、基于UDP面向无连接的通信

udp通信是面向无连接的,不可靠的,尽最大努力传输的通信

传输过程中可能出现数据丢失、重复、失序

创建套接字时,使用的传输层名称为SOCK_DGRAM

  • 原理
1
2
3
4
5
6
7
8
9
10
服务器端:
1.socket():创建用于连接的套接字
2.bind():绑定ip和端口号
3.recvfrom()/sendto():收发数据
4.close():关闭套接字
客户端:
1.socket():创建用于连接的套接字
2.bind():绑定ip和端口号
3.recvfrom()/sendto():收发数据
4.close():关闭套接字
  • UDP服务器端和客户端的实现

服务器端:

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
#include <myhead.h>
#define SER_PORT 8888 // 服务器端口(必须绑定,客户端需知道此端口)
#define SER_IP "0.0.0.0" // 监听所有网卡
#define CLI_PORT 9999
#define CLI_IP "127.0.0.1"

int main(int argc, const char *argv[])
{
// 1. 创建UDP套接字(type为SOCK_DGRAM)
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_DGRAM表示UDP协议
if(sfd == -1)
{
perror("socket error");
return -1;
}

// 2. 绑定服务器地址(UDP服务器必须绑定,否则客户端无法发送数据)
struct sockaddr_in sin; // 服务器自身地址信息
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT); // 绑定服务器端口(修正:原代码误用CLI_PORT)
sin.sin_addr.s_addr = inet_addr(SER_IP); // 绑定服务器IP
if(bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

// 3. 数据收发(UDP无连接,每次收发需指定对方地址)
char rbuf[128] = "";
struct sockaddr_in cin; // 存储客户端地址(用于回传数据)
socklen_t socklen = sizeof(cin); // 地址长度

while(1)
{
bzero(rbuf, sizeof(rbuf));
// 接收数据:从任何客户端接收,同时获取客户端地址
// recvfrom参数:套接字、接收缓冲区、长度、标志、客户端地址、地址长度
if(recvfrom(sfd, rbuf, sizeof(rbuf)-1, 0, (struct sockaddr*)&cin, &socklen) == -1)
{
perror("recvfrom error");
return -1;
}
printf("收到[%s:%d]消息:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);

// 拼接响应并发送给客户端(需指定客户端地址)
strcat(rbuf, " :)");
sendto(sfd, rbuf, strlen(rbuf), 0, (struct sockaddr*)&cin, sizeof(cin));
printf("发送成功\n");
}

// 4. 关闭套接字
close(sfd);
return 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
#include <myhead.h>
#define SER_PORT 8888 // 服务器端口(必须正确,否则无法发送到服务器)
#define SER_IP "127.0.0.1" // 服务器IP
#define CLI_PORT 9999 // 客户端端口(可选,系统可自动分配)
#define CLI_IP "127.0.0.1"

int main(int argc, const char *argv[])
{
// 1. 创建UDP套接字
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}

// 2. 绑定客户端地址(可选:UDP客户端可省略,系统自动分配端口)
struct sockaddr_in cin;
cin.sin_family = AF_INET;
cin.sin_port = htons(CLI_PORT); // 客户端端口
cin.sin_addr.s_addr = inet_addr(CLI_IP); // 客户端IP
if(bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
{
perror("bind error"); // 客户端绑定失败不影响发送(可注释)
return -1;
}
printf("bind success\n");

// 3. 数据收发(需指定服务器地址)
char wbuf[128] = "";
struct sockaddr_in sin; // 服务器地址信息(发送目标)
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT); // 服务器端口(网络字节序)
sin.sin_addr.s_addr = inet_addr(SER_IP); // 服务器IP(网络字节序)

while(1)
{
bzero(wbuf, sizeof(wbuf));
// 从标准输入获取数据
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0'; // 去除换行符

// 发送数据到服务器(必须指定服务器地址)
if(sendto(cfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("sendto error");
break;
}
printf("发送成功\n");

// 接收服务器响应(UDP无连接,需确认服务器地址)
bzero(wbuf, sizeof(wbuf));
// 此处不关心响应来源,故地址参数为NULL
if(recvfrom(cfd, wbuf, sizeof(wbuf)-1, 0, NULL, NULL) == -1)
{
perror("recvfrom error");
return -1;
}
printf("收到消息:%s\n", wbuf);
}

// 4. 关闭套接字
close(cfd);
return 0;
}

二、TCP并发服务器

上面写的代码样例中,有一些问题,TCP服务器端并不能连接多个客户端,接下来就是要引入并发操作来解决该问题

一、循环服务器

如果你已经理解了基本的TCP模型,那么你肯定能看出来问题就出在了accept只进行了一次,那好解决啊,循环不就行了吗?试试看再说

TCP服务器端:

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
74
75
76
77
78
79
80
81
#include <myhead.h>  // 自定义头文件
#define SER_PORT 8888 // 服务器端口号(1024以上,避免VIP端口权限问题)
#define SER_IP "0.0.0.0" // 监听所有可用网卡IP(INADDR_ANY的点分十进制形式)

int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字
// domain:AF_INET(IPv4协议族);type:SOCK_STREAM(TCP流式套接字);protocol:0(默认使用TCP协议)
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1) // 错误判断:创建失败返回-1
{
perror("socket error"); // 打印错误信息(如权限不足、协议不支持等)
return -1;
}

// 2. 绑定IP和端口号(服务器必须绑定,否则客户端无法定位)
struct sockaddr_in sin; // 存储IPv4地址信息的结构体
sin.sin_family = AF_INET; // 协议族:必须与socket创建时一致
sin.sin_port = htons(SER_PORT); // 端口号:转换为网络字节序(大端)
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址:转换为网络字节序(0.0.0.0表示监听所有网卡)

// 绑定操作:将套接字与地址信息关联
if(bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error"); // 常见错误:端口被占用、IP不可用
return -1;
}
printf("bind success\n");

// 3. 启动监听(将主动套接字转为被动套接字)
// 第二个参数:已完成连接队列的最大长度(半连接队列长度由系统决定)
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4. 阻塞等待客户端连接(从已连接队列中取出连接)
struct sockaddr_in cin; // 存储客户端地址信息的结构体
socklen_t socklen = sizeof(cin); // 地址结构体长度(传入传出参数)
// 注意:accept返回新的套接字(newfd),用于与该客户端通信,原sfd继续监听新连接
int newfd = accept(sfd, (struct sockaddr*)&cin, &socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
// 打印客户端信息:inet_ntoa转换IP为点分十进制,ntohs转换端口为本地字节序
printf("[%s:%d]:连接成功!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

// 5. 数据收发(循环处理)
char rbuf[128] = ""; // 接收缓冲区
while(1)
{
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区(避免残留旧数据)
// 接收数据:从newfd读取客户端发送的数据,返回值为实际接收字节数
int res = recv(newfd, rbuf, sizeof(rbuf)-1, 0); // 留1字节存'\0'
if(res == 0) // 返回0表示客户端正常关闭连接
{
printf("对端已下线\n");
break;
}
printf("收到消息:%s\n", rbuf);

// 拼接响应数据并发送
strcat(rbuf, " :)");
// 发送数据:将处理后的数据发回客户端
if(send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}

// 6. 关闭套接字(释放资源)
close(newfd); // 关闭与客户端通信的套接字
close(sfd); // 关闭监听套接字
return 0;
}

就是这样,加一个while循环,似乎就解决了?这么简单,会出问题的吧?

执行代码后尝试连接多个服务端,问题出现了:与客户端1进行数据交换时,并不能处理客户端2的连接请求,因为在recv处阻塞了,除非客户端1下线。

这还是没解决问题,但是之前学过了实现并发可以用多进程、多线程,试试吧。

二、多进程实现并发

主进程用于完成对客户端的连接请求,子进程完成与客户端的数据交换。

是可靠多了,但是难点就在资源回收上了

  • 父进程阻塞回收(wait)时肯定出问题,因为第一个子进程不退出,父进程就卡住了
  • 那使用非阻塞回收(waitpid)吗?模拟一下,若仅仅有一个客户端,那父进程就会卡在accept处,即使子进程退出了,父进程也执行不到waitpid处了。
  • 多进程中有一个信号的概念,我在多进程那篇也提到了自动回收僵尸进程,这里就可以使用

TCP服务器端:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <myhead.h>      // 自定义头文件
#define SER_PORT 8888 // 服务器端口号(1024以上,避免VIP端口权限问题)
#define SER_IP "0.0.0.0" // 监听所有可用网卡IP(INADDR_ANY的点分十进制形式)

void handler(int signo)
{
if(signo == SIGCHLD)
{
while(waitpid(-1, NULL, WNOHANG) > 0);
}
}

int main(int argc, const char *argv[])
{
// 捕获SIGCHLD信号
if(signal(SIGCHLD, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}


// 1. 创建TCP套接字
// domain:AF_INET(IPv4协议族);type:SOCK_STREAM(TCP流式套接字);protocol:0(默认使用TCP协议)
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd == -1) // 错误判断:创建失败返回-1
{
perror("socket error"); // 打印错误信息(如权限不足、协议不支持等)
return -1;
}

// 2. 绑定IP和端口号(服务器必须绑定,否则客户端无法定位)
struct sockaddr_in sin; // 存储IPv4地址信息的结构体
sin.sin_family = AF_INET; // 协议族:必须与socket创建时一致
sin.sin_port = htons(SER_PORT); // 端口号:转换为网络字节序(大端)
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址:转换为网络字节序(0.0.0.0表示监听所有网卡)

// 绑定操作:将套接字与地址信息关联
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error"); // 常见错误:端口被占用、IP不可用
return -1;
}
printf("bind success\n");

// 3. 启动监听(将主动套接字转为被动套接字)
// 第二个参数:已完成连接队列的最大长度(半连接队列长度由系统决定)
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4. 阻塞等待客户端连接(从已连接队列中取出连接)
struct sockaddr_in cin; // 存储客户端地址信息的结构体
socklen_t socklen = sizeof(cin); // 地址结构体长度(传入传出参数)
while (1)
{

int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
if (newfd == -1)
{
perror("accept error");
return -1;
}
// 打印客户端信息:inet_ntoa转换IP为点分十进制,ntohs转换端口为本地字节序
printf("[%s:%d]:连接成功!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

pid_t pid = fork();
if (pid > 0)
{
// 父进程
// 用不到newfd
close(newfd);
}
else if (pid == 0)
{
// 子进程

// 5. 数据收发(循环处理)
char rbuf[128] = ""; // 接收缓冲区
while (1)
{
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区(避免残留旧数据)
// 接收数据:从newfd读取客户端发送的数据,返回值为实际接收字节数
int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0); // 留1字节存'\0'
if (res == 0) // 返回0表示客户端正常关闭连接
{
printf("对端已下线\n");
break;
}
printf("收到消息:%s\n", rbuf);

// 拼接响应数据并发送
strcat(rbuf, " :)");
// 发送数据:将处理后的数据发回客户端
if (send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}

// 6. 关闭套接字(释放资源)
close(newfd); // 关闭与客户端通信的套接字

exit(EXIT_SUCCESS);
}
else
{
perror("fork error");
return -1;
}
}
close(sfd); // 关闭监听套接字
return 0;
}

多进程可以使用,再试一下多线程吧

三、多线程实现并发

主线程用于处理客户端的连接请求,分支线程用于和客户端数据交换

同样是资源回收,将分支线程设置为分离态即可

TCP服务器端:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <myhead.h>      // 自定义头文件
#define SER_PORT 8888 // 服务器端口号(1024以上,避免VIP端口权限问题)
#define SER_IP "0.0.0.0" // 监听所有可用网卡IP(INADDR_ANY的点分十进制形式)

void *task(void *arg)
{
int newfd = *(int *)arg;
// 5. 数据收发(循环处理)
char rbuf[128] = ""; // 接收缓冲区
while (1)
{
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区(避免残留旧数据)
// 接收数据:从newfd读取客户端发送的数据,返回值为实际接收字节数
int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0); // 留1字节存'\0'
if (res == 0) // 返回0表示客户端正常关闭连接
{
printf("对端已下线\n");
break;
}
printf("收到消息:%s\n", rbuf);

// 拼接响应数据并发送
strcat(rbuf, " :)");
// 发送数据:将处理后的数据发回客户端
if (send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return NULL;
}
printf("发送成功\n");
}

// 6. 关闭套接字(释放资源)
close(newfd); // 关闭与客户端通信的套接字
//退出分支线程
pthread_exit(NULL);
}


int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字
// domain:AF_INET(IPv4协议族);type:SOCK_STREAM(TCP流式套接字);protocol:0(默认使用TCP协议)
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd == -1) // 错误判断:创建失败返回-1
{
perror("socket error"); // 打印错误信息(如权限不足、协议不支持等)
return -1;
}

// 2. 绑定IP和端口号(服务器必须绑定,否则客户端无法定位)
struct sockaddr_in sin; // 存储IPv4地址信息的结构体
sin.sin_family = AF_INET; // 协议族:必须与socket创建时一致
sin.sin_port = htons(SER_PORT); // 端口号:转换为网络字节序(大端)
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址:转换为网络字节序(0.0.0.0表示监听所有网卡)

// 绑定操作:将套接字与地址信息关联
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error"); // 常见错误:端口被占用、IP不可用
return -1;
}
printf("bind success\n");

// 3. 启动监听(将主动套接字转为被动套接字)
// 第二个参数:已完成连接队列的最大长度(半连接队列长度由系统决定)
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4. 阻塞等待客户端连接(从已连接队列中取出连接)
struct sockaddr_in cin; // 存储客户端地址信息的结构体
socklen_t socklen = sizeof(cin); // 地址结构体长度(传入传出参数)
while(1)
{

}
int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
if (newfd == -1)
{
perror("accept error");
return -1;
}
// 打印客户端信息:inet_ntoa转换IP为点分十进制,ntohs转换端口为本地字节序
printf("[%s:%d]:连接成功!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

// 创建分支线程
pthread_t tid = -1;
if(pthread_create(&tid, NULL, &task, &newfd) != 0)
{
printf("pthread_create error\n");
return -1;
}

// pthread_join(tid, NULL); //阻塞回收?不能用
//将分支线程设置为分离态,系统自动回收
pthread_detach(tid);

close(sfd); // 关闭监听套接字
return 0;
}

四、IO多路复用

  • 引入:当主机没有操作系统,或程序不能使用多进程/多线程完成任务时,我们可以使用IO多路复用的技术来实现多任务并发

  • 理解IO多路复用,要先知道:事件和函数

    • 事件和函数的关系:用一个例子理解,scanf是阻塞函数,该函数完成的是输入事件
    • 当函数先于事件发生时,函数会阻塞等待事件的到来
    • 当函数后于事件发生时,函数就不会阻塞,直接执行
    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
    #include <myhead.h>

    int main(int argc, const char *argv[])
    {
    int num1 = 0, num2 = 0;

    printf("输入num1的值:");
    scanf("%d", &num1);
    printf("num1 = %d\n", num1);

    printf("输入num2的值:");
    scanf("%d", &num2);
    printf("num2 = %d\n", num2);

    return 0;
    }

    终端:
    [user]$ g++ 01test.cpp
    [user]$ ./a.out
    输入num1的值:1
    num1 = 1
    输入num2的值:2
    num2 = 2
    [user]$ ./a.out
    输入num1的值:1 2
    num1 = 1
    输入num2的值:num2 = 2
    • 可见,若事件先于函数,那么函数就不会阻塞,IO多路复用就是这个原理
  • IO多路复用的原理

    提供一个文件描述符集合,一个阻塞检测函数,用于检测集合中是否有事件产生,若有事件产生则解除阻塞,并且集合中就只剩下本次触发事件的文件描述符,接下来只需要判断哪些文件描述符在集合中并执行相关程序即可

  • 有三个函数可以实现IO多路复用

一、select

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
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
功能:阻塞等待文件描述符集合中是否有事件产生,如果有事件产生,则解除阻塞
参数1:文件描述符集合中,最大的文件描述符 加1
参数2、参数3、参数4:分别表示读集合、写集合、异常处理集合的起始地址
由于对于写操作而言,我们也可以转换读操作,所以,只需要使用一个集合就行
对于不使用的集合而言,直接填NULL即可
参数5:超时时间,如果填NULL表示永久等待,如果想要设置时间,需要定义一个如下结构体类型的变量,并将地址传递进去
struct timeval {
long tv_sec; /* 秒数 */
long tv_usec; /* 微秒 */
};
and
struct timespec {
long tv_sec; /* 秒数 */
long tv_nsec; /* 纳秒 */
};
返回值:
>0:成功返回解除本次阻塞的文件描述符的个数
=0:表示设置的超时时间,时间已经到达,但是没有事件事件产生
=-1:表示失败,置位错误码

//专门针对于文件描述符集合提供的函数
void FD_CLR(int fd, fd_set *set); //将fd文件描述符从容器set中删除
int FD_ISSET(int fd, fd_set *set);//判断fd文件描述符,是否存在于set容器中
void FD_SET(int fd, fd_set *set); //将fd文件描述符,放入到set容器中
void FD_ZERO(fd_set *set); //清空set容器

select实现TCP并发服务器:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <myhead.h>      // 自定义头文件(包含stdio.h、stdlib.h等常用头文件)
#define SER_PORT 8888 // 服务器端口号(1024以上,避免VIP端口权限问题)
#define SER_IP "0.0.0.0" // 监听所有可用网卡IP(INADDR_ANY的点分十进制形式)

int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字
// domain:AF_INET(IPv4协议族);type:SOCK_STREAM(TCP流式套接字);protocol:0(默认使用TCP协议)
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd == -1) // 错误判断:创建失败返回-1
{
perror("socket error"); // 打印错误信息(如权限不足、协议不支持等)
return -1;
}

// 2. 绑定IP和端口号(服务器必须绑定,否则客户端无法定位)
struct sockaddr_in sin; // 存储IPv4地址信息的结构体
sin.sin_family = AF_INET; // 协议族:必须与socket创建时一致
sin.sin_port = htons(SER_PORT); // 端口号:转换为网络字节序(大端)
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址:转换为网络字节序(0.0.0.0表示监听所有网卡)

// 绑定操作:将套接字与地址信息关联
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error"); // 常见错误:端口被占用、IP不可用
return -1;
}
printf("bind success\n");

// 3. 启动监听(将主动套接字转为被动套接字)
// 第二个参数:已完成连接队列的最大长度(半连接队列长度由系统决定)
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4. 阻塞等待客户端连接(从已连接队列中取出连接)
struct sockaddr_in cin; // 存储客户端地址信息的结构体
socklen_t socklen = sizeof(cin); // 地址结构体长度(传入传出参数)

// 定义文件描述符集合
fd_set readfds, tempfds;

// 清空文件描述符集合
FD_ZERO(&readfds);

// 将sfd文件描述符放到集合中
FD_SET(sfd, &readfds);

int maxfd = sfd; // 用于存储集合中最大文件描述符
int newfd = -1; // 用于接收accept创建的文件描述符
// 地址信息结构体数组,用于存储客户端地址信息
// 下标使用newfd即可,因为文件描述符的最小分配原则
struct sockaddr_in cin_arr[1024];

while (1)
{
// 将tempfds赋值成readfds,使用tempfds,方便操作
tempfds = readfds;

// 调用select函数
int res = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
if (res == -1)
{
perror("select error");
return -1;
}
else if (res == 0)
{
printf("time out!\n");
return -1;
}

// 阻塞解除,判断哪个文件描述符还在tempfds集合中即可
if (FD_ISSET(sfd, &tempfds))
{
// 注意:accept返回新的套接字(newfd),用于与该客户端通信,原sfd继续监听新连接
int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
if (newfd == -1)
{
perror("accept error");
return -1;
}
// 打印客户端信息
printf("连接成功!!\n");

// 存储已连接客户端地址信息
cin_arr[newfd] = cin;

// 将newfd放入集合中
FD_SET(newfd, &readfds);
// 更新maxfd
if (maxfd < newfd)
{
maxfd = newfd;
}
}

// 判断哪个客户端有数据发来
for (int i = 4; i <= maxfd; i++)
{
if (FD_ISSET(i, &tempfds))
{
// 5. 数据收发(循环处理)
char rbuf[128] = ""; // 接收缓冲区
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区(避免残留旧数据)
// 接收数据:从newfd读取客户端发送的数据,返回值为实际接收字节数
int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0); // 留1字节存'\0'
if (res == 0) // 返回0表示客户端正常关闭连接
{
printf("对端已下线\n");
// 关闭文件描述符
close(i);
// 从集合中去掉
FD_CLR(i, &readfds);
// 更新maxfd
for (int k = maxfd; k >= 0; i--)
{
if (FD_ISSET(k, &readfds))
{
maxfd = k;
break;
}
}
continue;
}
printf("收到消息:%s\n", rbuf);

// 拼接响应数据并发送
strcat(rbuf, " :)");
// 发送数据:将处理后的数据发回客户端
if (send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");

// 6. 关闭套接字(释放资源)
close(newfd); // 关闭与客户端通信的套接字
}
}
}

close(sfd); // 关闭监听套接字
return 0;
}

实际上,select并不经常用,一是因为其文件描述符集合最多放1024个文件描述符,二是每次调用需复制整个文件描述符集合。但是跨平台或简单场景还是能用的。

二、poll

poll比select稍灵活了些,下面是相关api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:阻塞等待文件描述符集合中是否有事件产生,如果有,则解除阻塞,返回本次触发事件的文件描述符个数
参数1:文件描述符集合容器的起始地址,是一个结构体数组,结构体类型如下
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 要等待的事件:由用户填写 */
short revents; /* 实际发生的事件 :调用函数结束后,内核会自动设置*/
};
关于事件对应的位:
POLLIN:读事件
POLLOUT:写事件
参数2:集合中文件描述符的个数
参数3:超时时间,负数表示永久等待,0表示非阻塞
返回值:
>0:表示触发本次解除阻塞事件的文件描述符的个数
=0:表示超时
=-1:出错,置位错误码

接下来我们使用poll解决一下tcp客户端输入阻塞和接收数据阻塞冲突的问题

TCP客户端:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <myhead.h>
#define SER_PORT 8888 // 服务器端口号(需与服务器一致)
#define SER_IP "127.0.0.1" // 服务器IP(本地测试用回环地址)
// 客户端端口和IP可省略绑定(系统自动分配),此处仅为示例
#define CLI_PORT 9999
#define CLI_IP "127.0.0.1"

int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字(与服务器端一致)
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
{
perror("socket error");
return -1;
}

// 2. 绑定客户端地址(可选:系统会自动分配端口和IP)
struct sockaddr_in cin;
cin.sin_family = AF_INET;
cin.sin_port = htons(CLI_PORT); // 客户端端口(可选)
cin.sin_addr.s_addr = inet_addr(CLI_IP); // 客户端IP(可选)
if (bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
{
perror("bind error"); // 客户端绑定失败不影响连接(可注释此段)
return -1;
}
printf("bind success\n");

// 3. 连接服务器
struct sockaddr_in sin; // 服务器地址信息
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT); // 服务器端口(网络字节序)
sin.sin_addr.s_addr = inet_addr(SER_IP); // 服务器IP(网络字节序)
// 发起连接:阻塞直到连接建立或失败
if (connect(cfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("connect error"); // 常见错误:服务器未启动、IP/端口错误
return -1;
}
printf("连接服务器成功\n");

struct pollfd pfds[2];
pfds[0].fd = 0; // 检测0号文件描述符,这里也就是检测fgets输入
pfds[0].events = POLLIN; // 检测读事件

pfds[1].fd = cfd; // 检测0号文件描述符,这里也就是检测recv接收
pfds[1].events = POLLIN; // 检测读事件

// 4. 数据收发
char wbuf[128] = ""; // 发送缓冲区
while (1)
{

// 阻塞检测
int res = poll(pfds, 2, -1);
if (res == -1)
{
perror("poll error");
return -1;
}

// 0号文件描述符的事件
if (pfds[0].revents == POLLIN)
{
bzero(wbuf, sizeof(wbuf));
// 从标准输入获取数据(用户输入)
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0'; // 去除fgets自带的换行符

// 发送数据到服务器
if (send(cfd, wbuf, strlen(wbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}

// cfd文件描述符的事件
// 有客户端发来消息了
if (pfds[1].revents == POLLIN)
{
// 接收服务器响应
bzero(wbuf, sizeof(wbuf));
int res = recv(cfd, wbuf, sizeof(wbuf) - 1, 0);
if (res == 0)
{
printf("服务器已下线\n");
break;
}
printf("收到服务器消息:%s\n", wbuf);
}
}

// 5. 关闭套接字
close(cfd);
return 0;
}

poll基本不怎么用,虽然比select稍灵活(文件描述符集合没有大小限制),但效率也不怎么高

三、epoll

上面两种的效率都比较低,这个epoll实现的IO多路复用效率就高了,因为其改进了工作方式,但不能跨平台了,只能用于linux

  • sellect和poll都是基于线性结构进行检测集合,而epoll是基于树形结构(红黑树)完成管理检测集合的

  • select和poll检测时,随着集合的增大,效率会越来越低。epoll使用的是函数回调机制,效率较高。处理文件描述符的效率也不会随着文件秒数的增大而降低。函数回调就是不用一直询问,等待事件被检测到了,由事件方通知哪个文件描述符发生了,加入就绪队伍即可。一个例子就是:你的肚子饿了会自己通知你,不用你一直问。

  • 和poll一样,epoll没有最大文件描述符的限制,仅仅受到程序能够打开的最大文件描述符数量限制

  • epoll只适用于linux平台,不能跨平台操作

  • 每次调用select()poll()时,都要把所有描述符从用户空间复制到内核空间,检查完再复制回用户空间。而epoll只需第一次调用epoll_ctl()注册描述符时,复制一次到内核。之后只有连接状态变化(比如有数据可读)时才更新

相关api

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

int epoll_create(int size);
功能:创建一个epoll实例,并返回该实例的句柄,是一个文件描述符
参数1:epoll实例中能够容纳的最大节点个数,自从linux 2.6.8版本后,size可以忽略,但是必须要是一个大于0的数字
返回值:成功返回控制epoll实例的文件描述符,失败返回-1并置位错误码

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:完成对epoll实例的控制

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞检测epoll实例中是否有文件描述符准备就绪,如果准备就绪了,就解除阻塞

epoll实现TCP并发服务器

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <myhead.h>      // 自定义头文件
#define SER_PORT 8888 // 服务器端口号(1024以上,避免VIP端口权限问题)
#define SER_IP "0.0.0.0" // 监听所有可用网卡IP(INADDR_ANY的点分十进制形式)

int main(int argc, const char *argv[])
{
// 1. 创建TCP套接字
// domain:AF_INET(IPv4协议族);type:SOCK_STREAM(TCP流式套接字);protocol:0(默认使用TCP协议)
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd == -1) // 错误判断:创建失败返回-1
{
perror("socket error"); // 打印错误信息(如权限不足、协议不支持等)
return -1;
}

// 2. 绑定IP和端口号(服务器必须绑定,否则客户端无法定位)
struct sockaddr_in sin; // 存储IPv4地址信息的结构体
sin.sin_family = AF_INET; // 协议族:必须与socket创建时一致
sin.sin_port = htons(SER_PORT); // 端口号:转换为网络字节序(大端)
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址:转换为网络字节序(0.0.0.0表示监听所有网卡)

// 绑定操作:将套接字与地址信息关联
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error"); // 常见错误:端口被占用、IP不可用
return -1;
}
printf("bind success\n");

// 3. 启动监听(将主动套接字转为被动套接字)
// 第二个参数:已完成连接队列的最大长度(半连接队列长度由系统决定)
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4. 阻塞等待客户端连接(从已连接队列中取出连接)
struct sockaddr_in cin; // 存储客户端地址信息的结构体
socklen_t socklen = sizeof(cin); // 地址结构体长度(传入传出参数)

int epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create error");
return -1;
}

struct epoll_event ev;
ev.data.fd = sfd;
ev.events = EPOLLIN;
// 将事件加入到集合中
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);

// 定义接收返回的事件集合
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);

while (1)
{
int num = epoll_wait(epfd, evs, size, -1);
printf("触发的事件个数num = %d\n", num);

for (int i = 0; i < num; i++)
{
int fd = evs[i].data.fd;
if (fd == sfd)
{
// 注意:accept返回新的套接字(newfd),用于与该客户端通信,原sfd继续监听新连接
int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
if (newfd == -1)
{
perror("accept error");
return -1;
}
// 打印客户端信息:inet_ntoa转换IP为点分十进制,ntohs转换端口为本地字节序
printf("[%s:%d]:连接成功!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

// 又多了newfd文件描述符
struct epoll_event ev;
ev.data.fd = newfd;
ev.events = EPOLLIN;
// 将事件加入到集合中
epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);
}
else // 代表是客户端的文件描述符就绪
{
// 5. 数据收发(循环处理)
char rbuf[128] = ""; // 接收缓冲区
bzero(rbuf, sizeof(rbuf)); // 清空缓冲区(避免残留旧数据)
// 接收数据:从newfd读取客户端发送的数据,返回值为实际接收字节数
int res = recv(fd, rbuf, sizeof(rbuf) - 1, 0); // 留1字节存'\0'
if (res == 0) // 返回0表示客户端正常关闭连接
{
printf("对端已下线\n");
// 将其从集合中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

// 关闭套接字
close(fd);
continue;// 不要用break;
}
printf("收到消息:%s\n", rbuf);

// 拼接响应数据并发送
strcat(rbuf, " :)");
// 发送数据:将处理后的数据发回客户端
if (send(fd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}
}
}

// 6. 关闭套接字(释放资源)
close(sfd); // 关闭监听套接字
close(epfd);
return 0;
}

epoll比另外两个效率高,肯定也更常用,但是跨主机时还是用select(我目前认知是这样的),接下来epoll还有两种工作模式:水平模式和边沿模式

epoll的两种工作模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1、水平模式:
简称LT模式(level trigered),是默认的一种初始模式,并且该模式支持阻塞和非阻塞的形式

在这种模式下,当文件描述符准备就绪后,内核会通知使用者哪些文件描述符就绪了。
使用者可以对就绪的文件描述符进行操作。
如果使用者不做任何操作或者没有全部处理完该文件描述符的信息,内核会继续通知使用者处理数据
特点:
对于读事件:如果该文件描述符中的缓冲区中的数据没有被读取完毕,则内核会继续解除阻塞,让用户继续处理,直到缓冲区中没有数据可以处理为止。
后面的解除阻塞,是自动完成的,无需用户进行对文件描述符的后续操作。
对于写事件:检测文件描述符缓冲区是否可以,如果可用则解除阻塞,一般写文件描述符的缓冲区都是可以的,一般不对写文件描述符进行检测

2、边沿模式
简称ET模式(edge trigered),需要手动设置该模式,并且该模式一般支持非阻塞形式
在种模式下,当文件描述符准备就绪后,内核会通知使用者哪些文件描述符就绪了,但是仅仅只通知一次
使用者可以对就绪的文件描述符进行操作
如果使用者不做任何操作,或者没有全部处理该文件描述符的信息,内核不会通知使用者再次处理数据,直到下一次该文件描述符的事件产生
特点:对于读事件:如果文件描述符中的缓冲区数据没有读取完毕,则内核也不会再次解除阻塞,直到下一次的该文件描述符事件产生,但是下一次的文件描述符的事件,读取的是上一次没有读取完毕的内容
对于写事件:检测文件描述符缓冲区是否可以,如果可用则解除阻塞,一般写文件描述符的缓冲区都是可以的,一般不对写文件描述符进行检测
边沿触发模式的处理效率会更高一些,要求用户必须一次性处理文件描述符中的数据

3、如何设置边沿触发:在将文件描述符放入到epoll树中时,需要加一个属性
struct epoll_event ev;
ev.event = EPOLLIN|EPOLLET;

上面字太多了,举个例子来说

水平模式就是这样的例子:接收用的字符数组容量仅有5(接收4个字符),但是客户端发来了15个字符,那么本次只能输出4个字符,下次该事件也会自动被检测到,直到15个字符输出完了

1
2
3
4
5
6
7
8
客户端:
hellohellohellohello
服务器端:
hell
ohel
lohe
lloh
ello

边沿模式就是:接收用的字符数组容量仅有5(接收4个字符),但是客户端发来了15个字符,那么本次只能输出4个字符,下次该事件不会被自动检测到,除非客户端再次发送消息了,但是服务器接收到的消息还是上次15个字符没接收完的部分

1
2
3
4
5
6
客户端:
hello12345
abcde
服务器端:
hell
o123

三、广播和组播

一、网络属性

  • 网络套接字在不同层中,有不同的设置,分别有应用层(套接字层)、传输层、网络层、以太网层
  • 对套接字属性进行设置,可以使用setsockopt和getsockopt函数完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
功能:设置或者获取套接字文件描述符的属性
参数1:套接字文件描述符
参数2:表示操作的套接字层
SOL_SOCKET:表示应用层或者套接字层,通过man 7 socket进行查找
该层中常用的属性:SO_REUSEADDR 地址快速重用
SO_BROADCAST 允许广播
SO_RCVTIMEO and SO_SNDTIMEO:发送或接收超时时间
IPPROTO_TCP:表示传输层的基于TCP的协议
IPPROTO_UDP:表示传输层中基于UDP的协议
IPPROTO_IP:表示网络层
该层常用的属性:IP_ADD_MEMBERSHIP,加入多播组
参数3:对应层中属性的名称
参数4:参数3属性的值,一般为int类型,其他类型,会给出
参数5:参数4的大小
返回值:成功返回0,失败返回-1并置位错误码

网络属性实例

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
#include <myhead.h>

int main(int argc, const char *argv[])
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1)
{
perror("socket error");
return -1;
}

int reuse = -1;
socklen_t reuselen;
// 检测是否能够地址快速重用
if(getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, &reuselen) == -1)
{
perror("getsocketopt error");
return -1;
}
printf("设置前:%d\n", reuse); // 0表示没有设置地址快速重用,1表示设置了

reuse = 1;
// 设置地址快速重用
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
// 检测是否能够地址快速重用
if(getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, &reuselen) == -1)
{
perror("getsocketopt error");
return -1;
}
printf("设置后:%d\n", reuse); // 0表示没有设置快速重用,1表示设置了

return 0;
}

二、广播

  • 像名称一样,是一对多的通信方式
  • 在当前网络下的所有主机都会收到该消息,无论是否愿意收到消息
  • 基于无连接的通信方式,UDP通信
  • 发送端要设置网络属性为允许广播(应用层),并向广播地址发送消息
  • 广播地址:网络号 + 全为1的主机号(也就是十进制的255),可以通过ifconfig查看

流程

1
2
3
4
5
6
7
8
9
10
11
12
服务器端:
1.socket创建用于发送数据的套接字
2.setsockopt设置套接字属性,允许广播setsockopt(sfd, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val));
3.定义套接字地址信息结构体为广播
4.sendto数据发送到广播
5.close关闭套接字
客户端:
1.socket创建用于发送数据的套接字
2.bind绑定ip和端口号
3.定义套接字地址信息结构体
4.recvfrom数据接收
close关闭套接字

发送端:

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 <myhead.h>

int main(int argc, const char *argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket error");
return -1;
}

int broad = 1;
// 设置允许广播
if(setsockopt(sfd, SOL_SOCKET, SO_BROADCAST, &broad, sizeof(broad)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("成功设置广播\n");

// 配置目标地址(广播地址)
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888); // 目标端口
// 使用子网广播地址(根据你的网络环境修改) 网络号+.255
rin.sin_addr.s_addr = inet_addr("192.168.111.255");

char wbuf[128] = "";
while(1)
{
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0';

sendto(sfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&rin, sizeof(rin));
printf("发送成功\n");

}
// 关闭套接字
close(sfd);
return 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
#include <myhead.h>

int main(int argc, const char *argv[])
{
int rfd = socket(AF_INET, SOCK_DGRAM, 0);
if(rfd == -1)
{
perror("socket error");
return -1;
}

int broad = 1;
// 设置允许广播
if(setsockopt(rfd, SOL_SOCKET, SO_BROADCAST, &broad, sizeof(broad)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("成功设置广播\n");

// 配置目标地址(广播地址)
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888); // 监听端口
// 使用INADDR_ANY监听所有接口
rin.sin_addr.s_addr = INADDR_ANY;
// 绑定ip和端口号
if(bind(rfd, (struct sockaddr*)&rin, sizeof(rin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

char rbuf[128] = "";
//定义容器接收对端的地址信息结构体
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1)
{
//清空容器
bzero(rbuf, sizeof(rbuf));

recvfrom(rfd, rbuf, sizeof(rbuf), 0, (struct sockaddr*)&cin, &socklen);
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);

}
// 关闭套接字
close(rfd);
return 0;
}

三、组播

  • 组播相较于广播,主机数量少了,就相当于一个村和一家人
  • 广播是给同一个网络下的所有主机发送消息,会占用大量的网络带宽,影响正常网络通信,造成网络拥塞
  • 组播是给同一个多播组的成员发送消息
  • 组播也是基于UDP实现的,发送者发送的消息,无论接收者愿不愿意,都会收到消息
  • 组播地址:D类网络地址 (224.0.0.0 — 239.255.255.255)

流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
发送端:
1.socket创建用于消息通信的套接字文件描述符
2.bind绑定(非必须)
3.填充要发送的地址信息结构体
4.sendto发送数据到组播
5.close关闭套接字
接收端:
1.socket创建用于通信的套接字文件描述符
2.setsockopt设置套接字加入多播组
3.bind绑定地址信息
4.recvfrom接收数据
5.close关闭套接字

设置加入多播组:
setsockopt(rfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr))
imr是一个结构体类型的变量,如下
struct ip_mreqn {
struct in_addr imr_multiaddr; /* 组播地址的网络字节序 */
struct in_addr imr_address; /* 接收端的主机地址 */
int imr_ifindex; /* 网卡设备编号 一般为2 可以通过指令 ip ad 查看 */
};

发送端:

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
#include <myhead.h>

int main(int argc, const char *argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket error");
return -1;
}

// 配置目标地址(组播地址)
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888); // 端口号
rin.sin_addr.s_addr = inet_addr("224.1.2.3");// 组播ip

char wbuf[128] = "";
//定义容器接收对端的地址信息结构体
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1)
{
bzero(wbuf, sizeof(wbuf));

fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0';

sendto(sfd, wbuf, sizeof(wbuf), 0, (struct sockaddr*)&rin, sizeof(rin));

}
// 关闭套接字
close(sfd);

return 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
#include <myhead.h>

int main(int argc, const char *argv[])
{
int rfd = socket(AF_INET, SOCK_DGRAM, 0);
if(rfd == -1)
{
perror("socket error");
return -1;
}

// 加入多播组
struct ip_mreqn imr;
imr.imr_address.s_addr = inet_addr("224.1.2.3"); // 组播ip
imr.imr_ifindex = 2;// 网卡编号,通过ip ad指令查看
// 这里设置自己的ip地址
imr.imr_multiaddr.s_addr = inet_addr("192.168.111.122");

if(setsockopt(rfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("成功加入多播组\n");

// 绑定ip地址和端口号
struct sockaddr_in rin;
rin.sin_addr.s_addr = inet_addr("224.1.2.3");// 组播ip
rin.sin_family = AF_INET;
rin.sin_port = htons(8888);

if(bind(rfd, (struct sockaddr*)&rin, sizeof(rin)) == -1)
{
perror("bind error");
return -1;
}

char rbuf[128] = "";
//定义容器接收对端的地址信息结构体
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1)
{
bzero(rbuf, sizeof(rbuf));

fgets(rbuf, sizeof(rbuf), stdin);
rbuf[strlen(rbuf) - 1] = '\0';

recvfrom(rfd, rbuf, sizeof(rbuf), 0, (struct sockaddr*)&cin, &socklen);
printf("[读取的消息为]:%s\n", rbuf);
}
// 关闭套接字
close(rfd);

return 0;
}

四、域套接字

一、域套接字是什么

  • 域套接字就像IPC一样,实现同主机下多个进程之间的通信
  • 不需要ip和端口号
  • 通过内核空间进行数据交换
  • 分为流式域套接字(基于TCP)和报式域套接字(基于UDP)
1
2
3
4
5
6
7
8
9
10
与网络套接字相关api的一点变化
int socket(int domain, int type, int protocol);
第一个参数不是AF_INET了,而是AF_UNIX
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第二个参数对应的结构体变了:
struct sockaddr_un {
sa_family_t sun_family; /* 通信域:AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 通信使用的文件 */ 要求套
接字文件不存在
};

相关api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>

int access(const char *pathname, int mode);
功能:判断给定的文件是否具有给的的权限
参数1:要被判断的文件路径
参数2:要被判断的权限
R_OK:读权限
W_OK:写权限
X_OK:执行权限
F_OK:是否存在
返回值:如果要被判断的权限都存在,则返回0,否则返回-1并置位错误码

int unlink(const char *path);
功能:删除指定的文件
参数:要删除的文件路径
返回值:成功删除返回0,失败返回-1并置位错误码

二、流式域套接字

服务器端:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <myhead.h>

int main(int argc, const char *argv[])
{

// 1、创建用于连接的套接字文件描述符
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 参数1:AF_INET表示使用的是IPv4的通信域
// 参数2:SOCK_STREAM表示使用的是tcp通信
// 参数3:由于参数2指定了协议,参数3填0

if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", sfd);

// 创建套接字文件
if(access("./unix", F_OK) == 0)
{
if(unlink("./unix") == -1)
{
perror("unlink error");
return -1;
}
}

// 2、绑定ip地址和端口号
// 2.1 填充要绑定的ip地址和端口号结构体
struct sockaddr_un sun;
sun.sun_family = AF_UNIX; // 通信域
strcpy(sun.sun_path, "./unix"); // 必须保证存在

// 2.2 绑定工作
// 参数1:要被绑定的套接字文件描述符
// 参数2:要绑定的地址信息结构体,需要进行强制转换
// 参数3:参数2的大小
if(bind(sfd, (struct sockaddr*)&sun, sizeof(sun)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

// 3、启动监听
// 参数1:要启用监听的文件描述符
// 参数2:挂起队列的长度
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");

// 4、阻塞等待客户端的连接请求
// 定义变量,用于接收客户端地址信息结构体
struct sockaddr_un cin;
socklen_t socklen = sizeof(cin);

// 参数1:服务器套接字文件描述符
// 参数2:用于接收客户端地址信息的结构体容器,不接收可用NULL
// 参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
int newfd = accept(sfd, (struct sockaddr*)&cin, &socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("accept success sun.sun_path : %s\n", sun.sun_path);

// 5、数据收发
char rbuf[128] = "";
while(1)
{
// 清空容器
bzero(rbuf, sizeof(rbuf));

// 从套接字中读取消息
int res = recv(newfd, rbuf, sizeof(rbuf), 0);
if(res == 0)
{
printf("对端已经下线\n");
break;
}
printf("[%s]:%s\n", cin.sun_path, rbuf);

// 对收到的数据处理一下,回给客户端
strcat(rbuf, " :)");

// 将消息发送给客户端
if(send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");

}

// 6、关闭套接字
close(newfd);
close(sfd);

return 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
74
75
76
77
78
#include <myhead.h>

int main(int argc, const char *argv[])
{

// 1、创建用于连接的套接字文件描述符
int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 参数1:AF_INET表示使用的是IPv4的通信域
// 参数2:SOCK_STREAM表示使用的是tcp通信
// 参数3:由于参数2指定了协议,参数3填0

if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success cfd = %d\n", cfd);

// 创建套接字文件
if(access("./unix", F_OK) == 0)
{
if(unlink("./unix") == -1)
{
perror("unlink error");
return -1;
}
}

// 2、绑定ip地址和端口号
// 2.1 填充要绑定的ip地址和端口号结构体
struct sockaddr_un sun;
sun.sun_family = AF_UNIX; // 通信域
strcpy(sun.sun_path, "./unix"); // 必须保证存在

// 2.2 绑定工作(客户端必须绑定,文件名是乱码)
// 参数1:要被绑定的套接字文件描述符
// 参数2:要绑定的地址信息结构体,需要进行强制转换
// 参数3:参数2的大小
if(bind(cfd, (struct sockaddr*)&sun, sizeof(sun)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

// 4、数据收发
char wbuf[128] = "";
while(1)
{
// 清空容器
bzero(wbuf, sizeof(wbuf));

// 从终端读取
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0';

// 发送消息
if(send(cfd, wbuf, strlen(wbuf), 0) == -1)
{
perror("send error");
return -1;
}

// 接收消息
if(recv(cfd, wbuf, sizeof(wbuf) - 1, 0) == 0)
{
printf("对端已下线");
break;
}
printf("收到消息:%s\n", wbuf);

}

// 6、关闭套接字
close(cfd);

return 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
#include <myhead.h>

int main(int argc, const char *argv[])
{
// 1、创建用于通信的套接字文件描述符
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
// SOCK_DGRAM表示基于udp
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", sfd);

// 创建套接字文件
if(access("./unix", F_OK) == 0)
{
if(unlink("./unix") == -1)
{
perror("unlink error");
return -1;
}
}
// 2、绑定ip地址和端口号
// 2.1 填充需要绑定的ip地址和端口号
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, "./unix");

// 2.2 绑定工作
// 参数1:要被绑定的套接字文件描述符
// 参数2:要被绑定的地址信息结构体,需要进行强制转换,防止警告
// 参数3:参数2的大小
if(bind(sfd, (struct sockaddr*)&sun, sizeof(sun)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

// 3、数据收发
char rbuf[128] = "";
// 定义容器接收对端的地址信息结构体
struct sockaddr_un cun;
socklen_t socklen = sizeof(cun);

while(1)
{
// 清空容器
bzero(rbuf, sizeof(rbuf));

// 从客户端中读取消息
if(recvfrom(sfd, rbuf, sizeof(rbuf), 0, (struct sockaddr*)&cun, &socklen) == -1)
{
perror("recvfrom error");
return -1;
}

// 处理一下
strcat(rbuf, " :)");

// 将数据发送给客户端
sendto(sfd, rbuf, strlen(rbuf), 0, (struct sockaddr*)&cun, sizeof(cun));
printf("发送成功\n");

}

// 4、关闭套接字
close(sfd);

return 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
74
75
76
77
78
79
#include <myhead.h>
#define SER_PORT 8888
#define SER_IP "192.168.228.136"
#define CLI_PORT 9999
#define CLI_IP "192.168.228.136"

int main(int argc, const char *argv[])
{
// 1、创建用于通信的套接字文件描述符
int cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success cfd = %d\n", cfd);

// 创建套接字文件
if(access("./unix", F_OK) == 0)
{
if(unlink("./unix") == -1)
{
perror("unlink error");
return -1;
}
}

// 2、绑定ip地址和端口号(可选)
// 2.1 填充要绑定的地址信息结构体
struct sockaddr_un cun;
cun.sun_family = AF_UNIX;
strcpy(cun.sun_path, "./linux");

// 2.2 绑定工作
if(bind(cfd, (struct sockaddr*)&cun, sizeof(cun)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

// 3、数据收发
char wbuf[128] = "";
// 填充服务器的地址信息结构体
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, "./unix");

while(1)
{
// 清空容器
bzero(wbuf, sizeof(wbuf));

// 从终端读入数据
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf) - 1] = '\0';

// 将数据发送给服务器
if(sendto(cfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&sun, sizeof(sun)) == -1)
{
perror("sendto error");
break;
}

// 接收服务器的消息
if(recvfrom(cfd, wbuf, sizeof(wbuf) - 1, 0, NULL, NULL) == -1)
{
perror("recvfrom error");
break;
}

printf("服务器发来的消息为:%s\n", wbuf);
}

// 4、关闭套接字
close(cfd);

return 0;
}

五、总结

本篇所有服务器模型都是基于TCP和UDP的服务器端和客户端的实现,所以说 一、网络编程基础 中讲解的TCP和UDP的服务器端和客户端的实现要掌握好,才能实现后面的并发、广播和组播、域套接字

本篇给了很多样例代码(讲解也大部分在代码中了),并没有详细介绍api,主要是太多了,现在字数已经1w6了

到此,网络编程结束。