📌 核心结论(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 断开,看看服务端是不是打印了“客户端断开”。
网硕互联帮助中心








评论前必须登录!
注册