📌 核心结论(TL;DR):
TCP 核心特性:面向连接(三次握手)、可靠传输、面向字节流。
服务端五步曲:
socket()->bind()->listen()->accept()->read()/write()。极其重要:
accept()成功后返回的是一个全新的文件描述符connfd,专门用于和该客户端通信,原来的listenfd继续监听。断线感知:TCP 中
read()返回 0,代表对端(客户端)主动关闭了连接,而不是出错。踩坑防范:服务端重启常遇
Address already in use,需用setsockopt()设置SO_REUSEADDR端口复用。
一、 TCP 核心流程解析
TCP 就像打电话,必须先建立连接。
服务器端:
socket创建电话机,bind插卡绑定固定号码,listen开机待机,accept接听。客户端:
socket创建电话机,connect直接拨号。
(注:客户端不需要手动bind,connect时操作系统会自动分配随机端口)
二、 为什么 accept 要返回新的套接字?
listen_fd是负责监听是否有新连接进来的(只负责拉客),而accept返回的conn_fd才是真正用来和当前客户端通信的。如果不这样做,服务器就无法同时为多个客户端服务。
三、 实战:单进程 TCP Echo 服务器
(这里只展示核心逻辑,完整代码见文末)
// 端口复用,解决重启报错问题 int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 接收连接 int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); // 通信与断线判断 int n = read(conn_fd, buffer, BUF_SIZE); if (n > 0) { // 正常收到数据 write(conn_fd, buffer, n); } else if (n == 0) { // 重点:n == 0 代表客户端关闭了连接 (收到了 FIN 包) printf("客户端已断开连接。\n"); break; }四、 踩坑记录:Address already in use
现象:强制
Ctrl+C结束服务器后,立刻重新运行,报错bind error: Address already in use。原因:TCP 连接断开时,主动断开方会进入
TIME_WAIT状态,占用端口一段时间(通常是 2MSL,约 1 分钟),防止最后一个 ACK 丢失导致连接混乱。解决:在
bind前调用setsockopt设置SO_REUSEADDR,允许端口复用。
五、 局限性
当前的服务器是单进程、单线程的。一旦accept成功,服务器就陷入了while(read)的阻塞。此时,第二个客户端连都连不进来。
自我实战操作
cd ~/linux_study/network_test服务端tcp_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 9999 #define BUF_SIZE 1024 int main() { int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 端口复用,防止重启时 Address already in use int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = INADDR_ANY; bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(listen_fd, 5); printf("TCP 服务器启动,监听端口 %d...\n", PORT); struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); // 核心:阻塞等待客户端连接 int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len); printf("接到客户端 %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); char buffer[BUF_SIZE]; while (1) { int n = read(conn_fd, buffer, BUF_SIZE - 1); if (n > 0) { buffer[n] = '\0'; printf("收到消息: %s\n", buffer); write(conn_fd, buffer, n); // Echo回显 } else if (n == 0) { // 核心:客户端断开 printf("客户端断开了连接。\n"); break; } } close(conn_fd); close(listen_fd); return 0; }客户端tcp_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" #define PORT 9999 #define BUF_SIZE 1024 int main() { int sock_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); return 1; } char buffer[BUF_SIZE]; while (1) { printf("请输入: "); fgets(buffer, BUF_SIZE, stdin); buffer[strcspn(buffer, "\n")] = 0; if (strcmp(buffer, "quit") == 0) break; write(sock_fd, buffer, strlen(buffer)); int n = read(sock_fd, buffer, BUF_SIZE - 1); if (n > 0) { buffer[n] = '\0'; printf("回显: %s\n", buffer); } } close(sock_fd); return 0; }编译运行:开两个终端,跑起服务端和客户端,发消息测试。测试完在客户端按Ctrl+C断开,看看服务端是不是打印了“客户端断开”。