文章目录
- 前言
- 一,服务器端流程
- 1-1 绑定协议
- 1-2 绑定IP和端口
- 1-3 监听客户端
- 1-4 接收连接
- 1-5 收发数据
- 1-6 关闭连接
- 1-7 服务端整体代码
- 二,客户端流程
- 2-1 指定地址和端口
- 2-2 连接服务器
- 2-3 发送消息
- 2-4 客户端整体代码
前言
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; // 本机任意 IPaddr.sin_port = htons(12345); // 端口 12345if (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!
本博客中只是做了一个简单的服务端和客户端的通信,在实际项目当中,这种通信代码还是前篇一律的,所以我就采用了分布式讲解