「socket编程学习笔记 | 2 封装与多进程」

本文最后更新于:2023年4月15日 晚上

socket编程学习笔记 | 2 封装与多进程

1 | 对socket进行封装

在头文件中包含如下类和方法声明:

定义TcpServer类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TcpServer {
private:
int m_socklen; // 结构体struct sockaddr_in的大小。
struct sockaddr_in m_clientaddr{}; // address of client
struct sockaddr_in m_serveraddr{}; // address of server
public:
int m_listenfd; // 监听socket
int m_connfd; // 客户端连接socket
bool m_btimeout; // 是否超时, true: timeout false: not time out
int m_buflen{}; // 调用Read方法后,接收到的报文的大小,单位:字节。

TcpServer();
bool InitServer(unsigned int port);
bool Accept(); //
char *GetIP() const; // 获取客户端ip地址
bool Read(char *buffer,int itimeout); // 读取数据, itimeout为超时时间, 设置为0为无限等待
bool Write(const char *buffer,int ibuflen=0); // 发送数据, 发送ascii字符串时ibuflen取0, 发送二进制流时取二进制数据块大小(单位: byte)
void CloseListen(); // 关闭监听socket即m_listenfd
void CloseClient(); // 关闭客户端socket即m_connfd
~TcpServer();
};

定义TcpClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TcpClient {
public:
int m_sockfd; // 客户端socket
char m_ip[21]{}; // 服务器ip地址
int m_port; // 通信的端口
bool m_btimeout; // true-超时,false-未超时
int m_buflen{};
TcpClient();
bool ConnectToServer(const char* ip, int port);
bool Read(char* buffer, int itimeout = 0);
bool Write(const char* buffer, int ibuflen = 0);
void Close();
~TcpClient();
};

定义读写实用方法:

1
2
3
4
5
6
7
8
9
// communication utils
// Readn: 从sockfd中读取n字节数据到buffer
bool Readn(int sockfd, char* buffer, size_t n);
// Writen: 向sockfd发送buffer中n字节数据
bool Writen(int sockfd, const char* buffer, size_t n);
// TcpRead: Tcp接受数据, itimeout为超时时间, 0为无限等待
bool TcpRead(int sockfd, char* buffer, int* ibuflen, bool& btimeout, int itimeout = 0);
// TcpWrite: 发送数据, 发送ascii字符串时ibuflen取0, 发送二进制流时取二进制数据块大小(单位: byte)
bool TcpWrite(int sockfd, const char* buffer, int ibuflen = 0);

2 | 对粘包和拆包的处理

什么是粘包:

  1. 发送端需要等缓冲区满才发送出去,造成粘包 (发送端出现粘包)

  2. 接收端没有及时接收缓冲区包数据,造成一次性接收多个包,出现粘包 (接收端出现粘包)

如何处理粘包:

发送消息的时候在消息首部四个字节表示消息的长度, 接受方每次读取消息的时候先读取首部, 然后按长度读取消息

实现:

TcpRead中「拆包」:

1
2
3
4
5
(*ibuflen) = 0;
if (!Readn(sockfd, (char *) ibuflen, 4)) return false; // 读取首部四个字节, 知道该段数据长度
(*ibuflen) = ntohl(*ibuflen); // net to host long, 网络字节序->主机字节序
if (!Readn(sockfd, buffer, (*ibuflen))) return false;
return true;

TcpWrite中「封包」:

1
2
3
4
5
6
7
8
9
10
int ilen = 0;
if (ibuflen == 0) ilen = strlen(buffer); // 消息的长度
else ilen = ibuflen;
int ilenn = htonl(ilen); // host to net long, 主机字节序->网络字节序
char str2buffer[ilen + 4];
memset(str2buffer, 0, sizeof(str2buffer));
memcpy(str2buffer, &ilenn, 4); // 把消息长度写入str2buffer的前四个字节
memcpy(str2buffer + 4, buffer, ilen); // 消息写入str2buffer
if (!Writen(sockfd, str2buffer, ilen + 4)) return false;
return true;

3 | 多进程socket

服务端多进程, 每来一个客户端为该客户端创建一个进程:

linux下使用fork()创建子进程, 当返回值>0表示父进程, 返回值=0表示子进程

这里的逻辑是先while (!server.Accept());监听到一个客户端的连接后, 创建一个子进程, 由于子进程拷贝了父进程的资源故父子进程都存在ListenSocketClientSocket, 为了节约资源, 父进程可以CloseClient()而子进程可以CloseListen()

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
//
// Created by roccoshi on 3/24/21.
//
#include "TcpUtil.h"

// 输入端口argv[1]
int main (int argc, char *argv[]) {
if (argc != 2) {
printf("please input the ip and port, usage: ./server [port]");
}
int port = atoi(argv[1]);
TcpServer server;
// 忽略子进程的信号, 避免产生僵尸进程
signal(SIGCHLD,SIG_IGN); // <wait/sys.h>
if (!server.InitServer(port)) {
printf("server.InitServer failed.\n"); return -1;
} else {
printf("The server port %d is listening...\n", port);
}

// 多进程
while (true) {
// waiting for client to connect
while (!server.Accept());
int ipid = fork();
// 父进程只需要保留监听socket
if (ipid > 0) {
server.CloseClient(); continue;
} else { // 子进程
// 子进程不需要监听socket
server.CloseListen();
printf("client(%s) connected. The pid is: %d\n",server.GetIP(), getpid());
char str_buffer[1024];
// 接受客户端申请的超时时间
if (!server.Read(str_buffer, 0)) exit(0);
int out_time = atoi(str_buffer);
printf("The client asked for %ds of service..\n",atoi(str_buffer));
while (true) {
memset(str_buffer,0,sizeof(str_buffer));
if (!server.Read(str_buffer, out_time)) break;
printf("%d recv: %s\n", getpid(), str_buffer);
memset(str_buffer,0,sizeof(str_buffer));
sprintf(str_buffer, "OK");
if (!server.Write(str_buffer)) break;
}
printf("the client %d disconnected.\n", getpid());
}
}
}

3-1 | 关于僵尸进程

当子进程退出时,内核都会给父进程发送一个SIGCHLD信号。如果父进程不做任何处理,那么退出的子进程将成为僵尸进程,这会在内核空间留下一些进程结构体的垃圾数据得不到清理,造成内存泄露。

如何避免僵尸进程:

我采用最简单的方式, 让父进程屏蔽SIGCHLD信号, 即:

1
2
// 忽略子进程的信号, 避免产生僵尸进程
signal(SIGCHLD,SIG_IGN); // <wait/sys.h>

此时子进程退出后由init进程托管, 清除相关资源

4 | 结果展示

一个server+一个client:

请求300s的服务时间, 之后交互信息:

加入多个客户端, 多进程:

image-20210328202527131

十个进程:

image-20210328202624819

超时机制演示:

image-20210328213740126


「socket编程学习笔记 | 2 封装与多进程」
https://blog.roccoshi.top/posts/53111/
作者
RoccoShi
发布于
2021年3月28日
许可协议