文章目录
前言
TCP 的通信过程就像两个人打电话:客户端先和服务端三次握手建立一条可靠的连接通道,之后数据会以字节流的形式在这条通道里双向传输,系统会负责把数据切片、编号、确认和重传,保证信息不丢失、不重复、按顺序送达,最后通过四次挥手优雅地断开连接。
连接流程大致如下图

一,服务器端流程
1-1 绑定协议
TCP通信也和UDP一样需要先创建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
这里的SOCK_STREAM,说明套接字是用字节流的形式传递数据,也就是TCP通信
1-2 绑定IP和端口
sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 本机任意 IP
addr.sin_port = htons(12345); // 端口 12345
if (bind(server_fd, (sockaddr * ) & addr, sizeof(addr)) < 0) {
perror("bind");
return 1;
}
addr.sin_family = AF_INET;的意思就是,设置协议为IPv4,其它的不过多讲解就是绑定端口和将sockaddr_in转化为sockaddr
addr.sin_addr.s_addr = INADDR_ANY表示本机的客户端的任意IP都可以连接服务端
1-3 监听客户端
listen(server_fd, 5);
std::cout << "服务端启动,等待客户端连接..." << std::endl;
server_fd是我们socket创建的套接字,用于服务端得知是哪一个套接字要监听,所以必须传入 server_fd。没有 server_fd,内核就不知道“我要监听哪条网络通道”,就没法接受连接
这里的这个参数5表明:连接队列的最大长度,内核会维护一个队列,存放已到达但还没被 accept() 处理的客户端连接,这里写 5 表示最多允许排队 5 个连接请求。超过的请求可能会被拒绝。
1-4 接收连接
sockaddr_in client_addr{};
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (sockaddr*)&client_addr, &len);
我们需要接收客户端的连接,这时会用到 sockaddr_in 来存储客户端的地址信息。accept() 会返回一个新的套接字 client_fd。因此服务器中会有 两种套接字:
-
server_fd:用于监听端口和接受客户端的连接请求。它可以同时管理多个客户端连接,但 不能直接用于数据通信。 -
client_fd:由accept()返回,用于和 特定客户端 进行通信。每个客户端连接都会对应一个独立的client_fd,可以通过read()和write()发送或接收数据。
简而言之:server_fd 用于建立连接,client_fd 用于通信,server_fd 可以服务多个客户端,而每个 client_fd 只对应一个客户端。
1-5 收发数据
char buffer[1024];
int n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "收到客户端消息: " << buffer << std::endl;
std::string reply = "Hello from server!";
write(client_fd, reply.c_str(), reply.size());
}
先是读取数据,我们先定义一个buffer,将对应客户端的套接字对应的文件信息读取到buffer中,再使用write将数据发送到client_fd对应的套接字当中,这里其实没什么特别的,就是收数据/发数据
1-6 关闭连接
close(client_fd);
close(server_fd);
虽然只有一行,但是OS这里会让客户端和服务端发生四次挥手:
-
FIN(发送方): 服务端调用
close(client_fd)内核向客户端发送FIN报文,表示“我已经没有数据要发了”。 -
ACK(接收方): 客户端收到
FIN后,回复ACK报文,确认收到,客户端仍然可以发送剩余数据。 -
FIN(接收方): 客户端发送完数据后,也调用
close(),向服务端发送FIN报文,表示“我也发送完了”。 -
ACK(发送方): 服务端收到
FIN后,发送ACK报文 确认。连接真正关闭。
1-7 服务端整体代码
int main() {
// 1. 创建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return 1;
}
// 2. 绑定 IP 和端口
sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 本机任意 IP
addr.sin_port = htons(12345); // 端口 12345
if (bind(server_fd, (sockaddr * ) & addr, sizeof(addr)) < 0) {
perror("bind");
return 1;
}
// 3. 监听
listen(server_fd, 5);
std::cout << "服务端启动,等待客户端连接..." << std::endl;
// 4. 接受连接
sockaddr_in client_addr {};
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (sockaddr * ) & client_addr, & len);
if (client_fd < 0) {
perror("accept");
return 1;
}
std::cout << "客户端已连接!" << std::endl;
// 5. 收发数据
char buffer[1024];
int n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "收到客户端消息: " << buffer << std::endl;
std::string reply = "Hello from server!";
write(client_fd, reply.c_str(), reply.size());
}
// 6. 关闭连接
close(client_fd);
close(server_fd);
return 0;
}
二,客户端流程
在客户端流程当中存在和服务端一样的流程,就是创建套接字,这里我们直接省略了。
2-1 指定地址和端口
sockaddr_in server_addr {};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
inet_pton(AF_INET, "127.0.0.1", & server_addr.sin_addr); // 本地回环地址
这里我们创建的inet_pton的作用是把点分十进制的 IP 地址字符串转换成网络字节序的二进制形式
因为 socket 系统调用只能处理二进制的网络地址,而不能直接识别字符串形式的 IP,人类习惯用 "127.0.0.1" 这样的点分十进制 IP。
内核底层在发送数据时,需要 32 位的二进制形式(网络字节序)来表示 IP 地址,inet_pton 就完成了 “人类可读 IP → 网络可用二进制 IP” 的转换,如果不做转换,connect() 或 bind() 会因为地址无效而失败
2-2 连接服务器
我们通过connet来与服务端建立连接
if (connect(sock, (sockaddr * ) & server_addr, sizeof(server_addr)) < 0) {
perror("connect");
return 1;
}
这里没有什么很特别的,就是传递我们的地址端口接口体给服务端,然后服务端拿到结构体和客户端进行连接,并且客户端的sock套接字也和服务端的套接字对应的文件进行连接
2-3 发送消息
std::string msg = "Hello Server!";
write(sock, msg.c_str(), msg.size());
char buffer[1024];
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "收到服务端回复: " << buffer << std::endl;
}
通过write和read发送和接收消息,也是很简单的代码
2-4 客户端整体代码
int main() {
// 1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
// 2. 指定服务端地址
sockaddr_in server_addr {};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
inet_pton(AF_INET, "127.0.0.1", & server_addr.sin_addr); // 本地回环地址
// 3. 连接服务器
if (connect(sock, (sockaddr * ) & server_addr, sizeof(server_addr)) < 0) {
perror("connect");
return 1;
}
std::cout << "已连接服务器!" << std::endl;
// 4. 发送消息
std::string msg = "Hello Server!";
write(sock, msg.c_str(), msg.size());
// 5. 接收回复
char buffer[1024];
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "收到服务端回复: " << buffer << std::endl;
}
// 6. 关闭
close(sock);
return 0;
}
演示结果:
服务端
root@hcss-ecs-f59a:/gch/code/HaoHao/learn3/day6# ./server
服务端启动,等待客户端连接...
客户端已连接!
收到客户端消息: Hello Server!
客户端
root@hcss-ecs-f59a:/gch/code/HaoHao/learn3/day6# ./client
已连接服务器!
收到服务端回复: Hello from server!
本博客中只是做了一个简单的服务端和客户端的通信,在实际项目当中,这种通信代码还是前篇一律的,所以我就采用了分布式讲解
1043

被折叠的 条评论
为什么被折叠?



