💖 点赞 + 收藏 + 关注,Socket 工业级网络编程全套实战持续更新!
✨ 本专栏 全程手写源码、无阉割、纯工程
0. 前言
上一章我们理解了 Socket 的核心概念、TCP/UDP 选型以及 TCP 流式特性。理论看懂只是第一步,本章我们正式上手 Linux 下原生 Socket API,从零实现一套标准阻塞式 TCP 客户端与 TCP 服务端完整代码。
很多初学者直接复制网上 Demo,但是根本不懂:
本章逐行拆解每一个系统调用,代码可直接在 Ubuntu、Linux 开发板编译运行,同时讲解移植到嵌入式 MCU(LWIP)的兼容要点。
开发环境:Ubuntu 20.04 / GCC 编译器,编译命令:gcc server.c -o server 、gcc client.c -o client
1. TCP 阻塞通信整体流程
服务端完整流程
客户端完整流程
重点区分:listen、accept 只有服务端需要调用;connect 只有客户端调用。
2. 核心 API 详细讲解
2.1 socket () 创建套接字
int socket(int domain, int type, int protocol);
- domain:地址族,AF_INET = IPv4 协议
- type:套接字类型,SOCK_STREAM = TCP 流式套接字;SOCK_DGRAM = UDP 数据报套接字
- protocol:协议类型,TCP 填 0 即可
- 返回值:成功返回文件描述符 fd(int),失败返回 -1
2.2 sockaddr_in IPv4 地址结构体
所有绑定、连接都需要填充这个结构体:
struct sockaddr_in {
sa_family_t sin_family; // 地址族 AF_INET
in_port_t sin_port; // 端口号,必须网络字节序
struct in_addr sin_addr; // IPv4地址
unsigned char sin_zero[8]; // 保留填充,补齐字节
};
struct in_addr {
uint32_t s_addr; // ip地址,网络字节序
};
通用结构体 struct sockaddr 是为了兼容 IPv4/IPv6,实际开发优先使用 sockaddr_in,强制类型转换传入 bind/connect。
2.3 字节序转换函数(工程必用)
CPU 存储分为主机字节序(小端),网络传输统一规定 大端(网络字节序),必须转换,否则通信异常!
- htons(uint16_t hostshort):host to network short,端口转换(16 位)
- htonl(uint32_t hostlong):host to network long,IP 转换(32 位)
- ntohs / ntohl:网络字节序转主机字节序
INADDR_ANY:宏定义 0.0.0.0,表示绑定本机所有网卡 IP,多网卡设备推荐使用。
2.4 bind () 绑定地址端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket 创建返回的 fd;addr 地址结构体;addrlen 结构体大小 返回值:0 成功,-1 失败
常见报错:Address already in use 端口被占用,后续章节讲解端口复用 setsockopt
2.5 listen () 开启监听
backlog:内核等待连接队列最大长度,Linux 新版代表未完成握手 + 已完成握手队列总和,一般填 5、10 即可,不要过大。
2.6 accept () 阻塞等待连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
阻塞函数:没有客户端连接时,程序卡在 accept 不往下运行 成功返回 新的连接文件描述符 connfd,后续收发数据全部使用 connfd!
注意区分:sockfd 是监听套接字(一直保留),connfd 是每个客户端独立通信套接字
2.7 connect () 客户端发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端调用,向服务端 IP 端口发起三次握手。阻塞 API,握手成功返回 0。
2.8 read /write 数据收发
套接字可以当作普通文件描述符,使用 read、write:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
返回值说明(重中之重):
工程避坑:read 返回 0 必须关闭 fd,回收资源,防止句柄泄漏!
2.9 close () 关闭套接字
int close(int fd);
调用后触发 TCP 四次挥手,释放文件描述符资源。
3. 完整代码实战:TCP 阻塞服务端 server.c
#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 BUF_LEN 1024
int main(void)
{
int sockfd, connfd;
struct sockaddr_in serv_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char recv_buf[BUF_LEN] = {0};
// 1. 创建TCP套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket create fail");
exit(EXIT_FAILURE);
}
// 填充服务端地址结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 2. 绑定端口IP
if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("bind fail");
close(sockfd);
exit(EXIT_FAILURE);
}
// 3. 开启监听
if (listen(sockfd, 5) < 0)
{
perror("listen fail");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server start success, listen port: %d\\n", PORT);
// 4. 阻塞等待客户端连接
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
if (connfd < 0)
{
perror("accept fail");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("client connect: %s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 5. 循环收发数据
while (1)
{
memset(recv_buf, 0, BUF_LEN);
int ret = read(connfd, recv_buf, BUF_LEN – 1);
if (ret > 0)
{
printf("recv data: %s\\n", recv_buf);
// 回发数据给客户端
write(connfd, recv_buf, ret);
}
else if (ret == 0)
{
printf("client disconnect!\\n");
break;
}
else
{
perror("read error");
break;
}
}
close(connfd);
close(sockfd);
return 0;
}
4. 完整代码实战:TCP 阻塞客户端 client.c
#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 SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUF_LEN 1024
int main(void)
{
int sockfd;
struct sockaddr_in serv_addr;
char send_buf[BUF_LEN] = {0};
char recv_buf[BUF_LEN] = {0};
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket create fail");
exit(EXIT_FAILURE);
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
// 将字符串IP转为网络字节序
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0)
{
perror("inet_pton fail");
close(sockfd);
exit(EXIT_FAILURE);
}
// 2. 连接服务端
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("connect fail");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("connect server success!\\n");
// 循环输入发送数据
while (1)
{
printf("please input send data:");
fgets(send_buf, BUF_LEN, stdin);
// 去除fgets自带换行符
send_buf[strcspn(send_buf, "\\n")] = '\\0';
write(sockfd, send_buf, strlen(send_buf));
memset(recv_buf, 0, BUF_LEN);
int ret = read(sockfd, recv_buf, BUF_LEN – 1);
if (ret > 0)
{
printf("server echo: %s\\n", recv_buf);
}
else if (ret == 0)
{
printf("server close connect!\\n");
break;
}
else
{
perror("read error");
break;
}
}
close(sockfd);
return 0;
}
5. 编译运行测试步骤
gcc server.c -o server
gcc client.c -o client
测试现象
客户端输入 hello socket 服务端打印收到数据,并回传给客户端显示。 客户端 ctrl+c 退出,服务端检测 read 返回 0,打印客户端断开。
6. 本章工程重点踩坑总结
拓展:本章是单客户端阻塞模型,无法支持多个客户端同时连接,后续 IO 多路复用章节解决并发问题
7. 本章小结
本章完整梳理阻塞 TCP 通信全流程,逐个拆解 Socket 基础 API 含义,实现了回声服务端与客户端可运行源码。 我们掌握了套接字创建、地址绑定、监听、连接建立、双向收发、连接关闭整套标准流程,理解阻塞 IO 基础特性。
遗留问题思考:
下一章:TCP 三次握手 & 四次挥手深度剖析|连接状态机、TIME_WAIT 核心工程问题




