4. TCP Socket编程(一):服务端与客户端创建流程
各位同学,今天我们来聊聊TCP Socket编程。说实话,这是网络编程里最基础也最核心的一块。我记得刚入行那会儿,第一次写Socket程序,折腾了一整天都没跑通——后来发现是bind的端口被占用了。嗯,这种坑踩过一次就记住了。
4.1 服务端创建流程:socket → bind → listen → accept
服务端的创建,说白了就是四个步骤。我习惯把这四个步骤记成「开、绑、听、接」——开一个socket,绑一个地址,听一个端口,接一个连接。咱们一步步来看。
4.1.1 socket():创建套接字
第一步,创建socket。这个函数就像是在操作系统里申请一个「网络通信的管道口」。
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- AF_INET:IPv4协议族。我个人习惯用AF_INET6表示IPv6,但大部分场景还是IPv4。
- SOCK_STREAM:流式套接字,对应TCP。如果是UDP就用SOCK_DGRAM。
- 0:协议自动选择,TCP的话填0就行。
4.1.2 bind():绑定地址和端口
第二步,绑定。你得告诉操作系统:「我这个socket要监听哪个IP和哪个端口」。
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_addr.sin_port = htons(8888); // 端口8888
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
这里有几个细节要注意:
- htonl()和htons():主机字节序转网络字节序。说白了就是大小端转换。不同CPU存数据的方式不一样,网络传输统一用大端序。
- INADDR_ANY:表示监听本机所有IP地址。如果你只想监听某个特定IP,可以填具体的地址。
- 端口号:小于1024的端口需要root权限。我建议开发阶段用8000-9000之间的端口,不容易冲突。
4.1.3 listen():进入监听状态
第三步,监听。bind之后,socket还不会主动接收连接。你得调用listen,告诉内核:「我准备好了,有连接就通知我」。
listen(sockfd, 5); // 第二个参数是backlog,表示连接队列的最大长度
backlog这个参数,很多人不理解。它其实是指「已完成三次握手但还没被accept取走的连接」的最大数量。如果队列满了,新的连接请求会被拒绝。
我建议backlog不要设得太小,也不要太大。太小了高并发时容易丢连接,太大了浪费内存。一般设128或256就够用了。
4.1.4 accept():接受客户端连接
第四步,接受连接。accept是个阻塞函数——如果没有客户端连上来,它会一直等在那里。
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
accept返回的是一个新的文件描述符,专门用来和这个客户端通信。原来的sockfd还在继续监听,可以接受其他连接。
4.2 客户端创建流程:socket → connect
客户端的流程就简单多了,两步走:创建socket,然后连接服务端。
4.2.1 socket():创建套接字
和服务端一样,客户端也要先创建一个socket。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
这里没什么特别的,参数和服务端完全一致。
4.2.2 connect():发起连接
connect函数负责发起三次握手。它会主动连接服务端的IP和端口。
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务端IP
server_addr.sin_port = htons(8888); // 服务端端口
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
connect也是阻塞的。它会一直等到三次握手完成,或者超时失败。如果服务端没启动,connect会返回-1,errno设置为ECONNREFUSED。
4.3 简单回显服务器实现
好了,理论讲完了,咱们写个完整的例子。回显服务器(echo server)是最简单的TCP应用——客户端发什么,服务端就原样返回什么。
4.3.1 服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 1. 创建socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket failed");
exit(1);
}
// 2. 设置地址重用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(1);
}
// 4. 监听
if (listen(server_fd, 5) < 0) {
perror("listen failed");
close(server_fd);
exit(1);
}
printf("Echo server listening on port %d...\n", PORT);
// 5. 接受连接
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
if (client_fd < 0) {
perror("accept failed");
close(server_fd);
exit(1);
}
printf("Client connected!\n");
// 6. 回显逻辑
int n;
while ((n = read(client_fd, buffer, BUFFER_SIZE)) > 0) {
write(client_fd, buffer, n);
}
// 7. 关闭连接
close(client_fd);
close(server_fd);
return 0;
}
4.3.2 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket failed");
exit(1);
}
// 2. 设置服务端地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(PORT);
// 3. 连接服务端
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(sockfd);
exit(1);
}
printf("Connected to server!\n");
// 4. 发送数据并接收回显
while (1) {
printf("Enter message: ");
fgets(buffer, BUFFER_SIZE, stdin);
write(sockfd, buffer, strlen(buffer));
int n = read(sockfd, buffer, BUFFER_SIZE);
if (n <= 0) break;
buffer[n] = '\0';
printf("Echo: %s", buffer);
}
close(sockfd);
return 0;
}
4.4 核心流程图
下面这张图展示了TCP Socket编程的完整流程。我特意把服务端和客户端放在一起对比,方便你理解。
4.5 关键点总结
| 函数 | 作用 | 注意事项 |
|---|---|---|
| socket() | 创建套接字 | 返回文件描述符,失败返回-1 |
| bind() | 绑定地址和端口 | 端口小于1024需root权限 |
| listen() | 进入监听状态 | backlog控制连接队列长度 |
| accept() | 接受客户端连接 | 阻塞函数,返回新fd |
| connect() | 发起连接 | 阻塞直到三次握手完成 |
好了,这一章的内容就到这儿。代码不多,但每一步都很关键。你想想看,整个互联网的通信,底层不就是靠这些socket在支撑吗?把这个基础打牢了,后面讲多路复用、线程池、异步IO的时候,你才能跟得上。