套接字和TCP
# 简单的 TCP 网络程序
# 实现
tcpServer.hpp
#include <arpa/inet.h>
#include <errno.h>
#include <strings.h>
#include <sys/socket.h>
#include <unistd.h>
#include "log.hpp"
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
namespace Server {
using namespace std;
enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR, OPEN_ERR, LISTEN_ERR, ACCEPT_ERR };
typedef function<void(int, string, uint16_t, string)> func;
static const int backlog = 5;
class tcpServer {
private:
// v1:基础版本
void DataIO(int sock) {
char buffer[1024];
while (true) {
// 读
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n == 0) {
logMessage(NORMAL, "client quit");
break;
}
buffer[n] = 0;
cout << "receive message:" << buffer << endl;
;
string response = buffer;
response += "[server send]";
// 写
write(sock, response.c_str(), response.size());
}
}
public:
tcpServer(const uint16_t& _port, const func& _callback) : port(_port), callback(_callback), listenfd(-1) {}
void initServer() {
// 1. 创建 socket 套接字
int n = 0;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
logMessage(FATAL, "create socket fail!");
exit(SOCKET_ERR);
}
// 2. 绑定端口号
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
n = bind(listenfd, (struct sockaddr*)&local, static_cast<socklen_t>(sizeof(local)));
if (n < 0) {
logMessage(FATAL, "bind internet error!");
exit(BIND_ERR);
}
// 3. 设置 socket 为监听状态
n = listen(listenfd, backlog);
if (n < 0) {
logMessage(FATAL, "listen socket error!");
exit(LISTEN_ERR);
}
}
void start() {
while (true) {
// 4. server 获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listenfd, (struct sockaddr*)&peer, &len);
if (sock < 0) {
logMessage(ERROR, "accept socket error!");
exit(ACCEPT_ERR);
}
cout << "listenfd:" << listenfd << endl;
cout << "sock:" << sock << endl;
// 5. TCP:面向字节流,文件操作
DataIO(sock);
// 要关闭已经使用过的 sock,否则会导致文件描述符泄露
close(sock);
}
}
~tcpServer() {
if (listenfd != -1) {
close(listenfd);
listenfd = -1;
}
}
private:
uint16_t port;
int listenfd; // 这个套接字不是用来数据通信的,而是用来监听链接,获取新链接的
func callback;
};
} // namespace Server
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
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
tcpClient.hpp
#include <arpa/inet.h>
#include <pthread.h>
#include <strings.h>
#include <sys/socket.h>
#include "log.hpp"
#include <errno.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
namespace Client {
using namespace std;
enum { USAGE_ERR = 1, SOCKET_ERR, CONNECT_ERR };
class tcpClient {
private:
public:
tcpClient(const string& _serverIp, const uint16_t& _serverPort)
: serverIp(_serverIp), serverPort(_serverPort), fd(-1) {}
void initClient() {
// 1. 创建套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
logMessage(FATAL, "socket create fail");
exit(SOCKET_ERR);
}
// 2. 不需要显示地绑定
// 3. 不需要 listen
// 4. 不需要 accept
}
void run() {
// 5. 需要发起链接
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
int n = connect(fd, (sockaddr*)&server, sizeof(server));
if (n != 0) {
logMessage(FATAL, "client connect error");
exit(CONNECT_ERR);
} else {
string message;
while (true) {
// 进行通信
cout << "Input# ";
getline(cin, message);
write(fd, message.c_str(), message.size());
char buffer[1024];
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = 0;
cout << "Server回显" << buffer << endl;
} else
break;
}
}
}
~tcpClient() {
if (fd != -1) {
close(fd);
fd = -1;
}
}
private:
string serverIp;
uint16_t serverPort;
int fd;
};
} // namespace Client
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
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
# 服务器
void initServer() {
// 1. 创建 socket 套接字
int n = 0;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
logMessage(FATAL, "create socket fail!");
exit(SOCKET_ERR);
}
// 2. 绑定端口号
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
n = bind(listenfd, (struct sockaddr*)&local, static_cast<socklen_t>(sizeof(local)));
if (n < 0) {
logMessage(FATAL, "bind internet error!");
exit(BIND_ERR);
}
// 3. 设置 socket 为监听状态
n = listen(listenfd, backlog);
if (n < 0) {
logMessage(FATAL, "listen socket error!");
exit(LISTEN_ERR);
}
}
void start() {
while (true) {
// 4. server 获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listenfd, (struct sockaddr*)&peer, &len);
if (sock < 0) {
logMessage(ERROR, "accept socket error!");
exit(ACCEPT_ERR);
}
cout << "listenfd:" << listenfd << endl;
cout << "sock:" << sock << endl;
// 5. TCP:面向字节流,文件操作
DataIO(sock);
// 要关闭已经使用过的 sock,否则会导致文件描述符泄露
close(sock);
}
}
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
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
注意:
- TCP 服务器需要时刻注意是否有客户端发来连接请求,此时就需要将 TCP 服务器创建的套接字设置为监听状态,其中 listen 中的 fd 叫做监听套接字
- TCP 服务器在与客户端进行网络通信之前,服务器需要先获取到客户端的连接请求
- accept 函数获取连接时可能会失败,但 TCP 服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接
- 套接字 Accept 机制 vs. 饭店迎宾流程对比
阶段 | 饭店迎宾场景 | Linux 网络套接字 | 核心功能 |
---|---|---|---|
准备阶段 | 开设饭店,明确地址 | 创建监听套接字 (socket ) | 建立服务端点 |
挂上招牌公布地址 | 绑定地址端口 (bind ) | 公布服务地址 | |
启动服务 | 开门营业准备接待 | 开始监听 (listen ) | 开启服务状态 |
设置等候区座位数 | 设置连接队列长度 (backlog ) | 定义接待容量 | |
客户到来 | 客人到店询问位置 | 客户端发起连接 (connect ) | 发起连接请求 |
安排客人在等候区 | 完成 TCP 三次握手 | 完成连接建立 | |
接待核心 | 服务员引导客人入座 | 调用 accept() | 接纳就绪连接 |
分配专属餐桌 | 返回新连接套接字 (conn_sock ) | 创建专属通道 | |
提供服务 | 服务员与餐桌客人交互 | 使用 conn_sock 通信 | 进行数据交换 |
迎宾员继续接待新客 | 监听套接字保持监听 | 维持监听状态 | |
服务结束 | 客人离开清理餐桌 | 关闭连接套接字 (close ) | 释放连接资源 |
饭店继续营业 | 服务器持续运行 | 服务持续进行 |
# 客户端
void initClient() {
// 1. 创建套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
logMessage(FATAL, "socket create fail");
exit(SOCKET_ERR);
}
// 2. 不需要显示地绑定
// 3. 不需要 listen
// 4. 不需要 accept
}
void run() {
// 5. 需要发起链接
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
int n = connect(fd, (sockaddr*)&server, sizeof(server));
if (n != 0) {
logMessage(FATAL, "client connect error");
exit(CONNECT_ERR);
} else {
string message;
while (true) {
// 进行通信
cout << "Input# ";
getline(cin, message);
write(fd, message.c_str(), message.size());
char buffer[1024];
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = 0;
cout << "Server回显" << buffer << endl;
} else
break;
}
}
}
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
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
注意:
- 客户端创建完套接字后需要向服务端发起连接请求 connect
# 单线程阻塞式服务器
void DataIO(int sock) {
char buffer[1024];
while (true) {
// 读
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n == 0) {
logMessage(NORMAL, "client quit");
break;
}
buffer[n] = 0;
cout << "receive message:" << buffer << endl;
;
string response = buffer;
response += "[server send]";
// 写
write(sock, response.c_str(), response.size());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这种处理方式中服务端只有服务完一个客户端后才会服务另一个客户端。当服务端调用 accept 函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是死循环,只能服务完当前客户端后才能继续服务下一个客户端。
因此接下来完成多进程版本地程序
# 多线程 TCP 网络程序
# 实现
tcpServer.hpp
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "log.hpp"
#include <errno.h>
#include <strings.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
namespace Server {
using namespace std;
enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR, OPEN_ERR, LISTEN_ERR, ACCEPT_ERR, FORK_ERR };
typedef function<void(int, string, uint16_t, string)> func;
static const int backlog = 5;
class tcpServer;
class tcpServerData {
public:
tcpServerData(tcpServer* _server, const int _sock) : server(_server), sock(_sock) {}
public:
tcpServer* server;
int sock;
};
class tcpServer {
private:
static void* clientThread(void* args) {
tcpServerData* serverData = static_cast<tcpServerData*>(args);
serverData->server->DataIO(serverData->sock);
close(serverData->sock);
delete serverData;
return nullptr;
}
public:
tcpServer(const uint16_t& _port, const func& _callback) : port(_port), callback(_callback), listenfd(-1) {}
void initServer() {
// 1. 创建 socket 套接字
int n = 0;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
logMessage(FATAL, "create socket fail!");
exit(SOCKET_ERR);
}
// 2. 绑定端口号
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
n = bind(listenfd, (struct sockaddr*)&local, static_cast<socklen_t>(sizeof(local)));
if (n < 0) {
logMessage(FATAL, "bind internet error!");
exit(BIND_ERR);
}
// 3. 设置 socket 为监听状态
n = listen(listenfd, backlog);
if (n < 0) {
logMessage(FATAL, "listen socket error!");
exit(LISTEN_ERR);
}
}
void start() {
while (true) {
// 4. server 获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listenfd, (struct sockaddr*)&peer, &len);
if (sock < 0) {
logMessage(ERROR, "accept socket error!");
exit(ACCEPT_ERR);
}
cout << "listenfd:" << listenfd << endl;
cout << "sock:" << sock << endl;
// 5. TCP:面向字节流,文件操作
// v2:多线程版本
pthread_t pid;
tcpServerData* serverData = new tcpServerData(this, sock);
pthread_create(&pid, nullptr, clientThread, (void*)serverData);
pthread_detach(pid);
}
}
void DataIO(int sock) {
char buffer[1024];
while (true) {
// 读
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n == 0) {
logMessage(NORMAL, "client quit");
break;
}
buffer[n] = 0;
cout << "receive message:" << buffer << endl;
;
string response = buffer;
response += "[server send]";
// 写
write(sock, response.c_str(), response.size());
}
// 关闭套接字,防止泄露
close(sock);
}
~tcpServer() {
if (listenfd != -1) {
close(listenfd);
listenfd = -1;
}
}
private:
uint16_t port;
int listenfd; // 这个套接字不是用来数据通信的,而是用来监听链接,获取新链接的
func callback;
};
} // namespace Server
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
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
# 多进程服务器
每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁,从而效率低下; 当有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多,CPU 的压力就越大,因为 CPU 要不断在这些线程之间来回切换,此时 CPU 在调度线程的时候,线程和线程之间切换的成本就会变得很高。
因此可以预先创建好一定数量的线程进行管理,接下来实现线程池服务器
# 线程池 TCP 网络程序
# 实现
Task.hpp
#pragma once
#include "log.hpp"
#include <cstdio>
#include <functional>
#include <iostream>
#include <string>
void DataIO(int sock) {
char buffer[1024];
while (true) {
// 读
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n == 0) {
logMessage(NORMAL, "client quit");
break;
}
buffer[n] = 0;
cout << "receive message:" << buffer << endl;
;
string response = buffer;
response += "[server send]";
// 写
write(sock, response.c_str(), response.size());
}
close(sock);
}
class Task {
using func_t = std::function<void(int)>;
public:
Task() {}
Task(int _sock, func_t _func = DataIO) : sock(_sock), callback(_func) {}
std::string operator()() {
callback(sock);
}
private:
int sock;
func_t callback;
};
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
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
创建任务类,服务器中初始化线程池,构建任务,推送到任务队列。
上次更新: 2025/09/03, 18:26:17