
简单概念
套接字Socket
简单通俗来说,Socket 就是 Linux 系统里,用来实现网络通信的“工具”或“端点”,可以想象成“两部电话之间的电话线+电话机”。有了 Socket,A电脑才能发数据给B电脑,B电脑才能收到数据。
标准来说,Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。
在 Linux 里,Socket 本质就是一个文件描述符(fd),和文件、串口、管道一样,用read/write/close操作。
Socket = {IP地址,端口号},一次网络通信必须要两个Socket:客户端socket与服务端socket,它们一一对应,双向通信。
TCP协议
TCP(Transmission Control Protocol 传输控制协议),具有以下三大特点:
- 面向连接:通信前必须先建立连接(三次握手)
- 可靠传输:不丢包、不乱序、出错重传
- 基于字节流:数据像水流一样连续,无边界→会产生粘包
TCP连接的生命周期:
- 三次握手→建立连接
- 数据传输→send(发送)/recv(接收)
- 四次挥手→断开连接
三次握手
目的:确保客户端和服务端的发送、接收能力都正常。TCP 连接在三次握手完成后才建立。

四次挥手
目的:安全关闭连接,保证数据收发完毕。

主动关闭连接的一方会进入TIME_WAIT持续 2MSL 作用:确保对方收到最后的ACK
核心流程
讲完了定义概念等方面,接下来我们讲代码。以下是服务端(server)与客户端(client)的固定 TCP 编程的步骤,需要熟记!我们在文章末端也放了一套测试代码,仅供参考。
在开始之前,我们还要先了解以下一些socket编程中几个常用的函数和结构体:
1. struct sockaddr_in结构体
struct sockaddr_in {
sa_family_t sin_family; // AF_INET(IPv4)
in_port_t sin_port; // 端口(必须htons)
struct in_addr sin_addr; // IP地址
};
2. 工具函数
htons():主机端口 → 网络端口(作用:填写端口号,给sin_port赋值)htonl():主机IP → 网络IP(常用在sin_addr)inet_addr():字符串IP → 网络IP(例如inet_addr("192.168.1.100");)inet_ntoa():网络IP → IP字符串(打印用inet_ntoa(client_addr.sin_addr))
服务端(server)
1. 定义变量,填写相关信息
struct sockaddr_in server_addr, client_addr;
// 清空地址结构体
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 填写服务端地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
在服务端中,我们需要填写自己的信息,供客户端识别连接
2. socket():创建套接字
// 创建套接字
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
- AF_INET:ipv4
- SOCK_STREAM:TCP协议
- 返回值为文件描述符fd
3. bind():绑定自己的IP和端口
// 绑定监听套接字
int ret = bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
作用:告诉操作系统“我要使用这个 IP 和端口”
4. listen():开启监听
ret = listen(server_sockfd, 128);
第二个参数表示内核中已完成三次握手、等待被accept的连接队列的最大长度。
5. accept():阻塞等待客户端连接
// 接受客户端连接
socklen_t client_addr_len = sizeof(client_addr);
int client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
handle_error("accept", client_sockfd);
printf("客户端 %s 已连接\n", inet_ntoa(client_addr.sin_addr));
- (struct sockaddr *)&client_addr:用来保存客户端的IP和端口
- &client_addr_len:客户端地址实际大小
- 返回值:新文件描述符(用于和客户端收发数据)
6. recv()/send():收发数据
// 从客户端读取消息的线程函数
void *read_from_client(void *arg)
{
int client_sockfd = *(int *)arg;
char buffer[1024];
while (1)
{
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_read = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_read < 0)
{
perror("recv");
break;
}
else if (bytes_read == 0)
{
printf("--------------------------\n");
printf("客户端已断开连接\n");
printf("--------------------------\n");
break;
}
printf("收到客户端消息: %s\n", buffer);
}
close(client_sockfd);
return NULL;
}
// 服务器向客户端发送消息的线程函数
void *write_to_client(void *arg)
{
int client_sockfd = *(int *)arg;
char buffer[1024];
while (1)
{
fgets(buffer, sizeof(buffer), stdin);
buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符
ssize_t bytes_sent = send(client_sockfd, buffer, strlen(buffer), 0);
if (bytes_sent < 0)
{
perror("send");
break;
}
}
return NULL;
}
int main(int argc,char *argv[])
{
pthread_t pid_read, pid_write;
// 创建线程处理客户端消息
pthread_create(&pid_read, NULL, read_from_client, (void *)&client_sockfd);
pthread_create(&pid_write, NULL, write_to_client, (void *)&client_sockfd);
// 等待线程结束
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
}
这里创建了两个线程,一个读线程,一个写线程,服务端既可以读取客户端发来的信息,也可以向客户端发送消息。
7. close():关闭套接字
close(server_sockfd);
close(client_sockfd);
最后关闭监听套接字和客户端套接字,我们完成了服务端代码的编写。
客户端(client)
1. 定义变量,填写需要连接的服务端信息
#define SERVERPORT 8080 //服务端的端口
#define SERVERIP "127.0.0.1" //服务端IP地址
struct sockaddr_in server_addr;
// 清空地址结构体
memset(&server_addr, 0, sizeof(server_addr));
// 填写连接服务端的地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVERPORT);
server_addr.sin_addr.s_addr = inet_addr(SERVERIP);
客户端的端口不需要填写,系统会自动分配其端口
2. socket():创建套接字
// 创建套接字
int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
3. connect():连接服务端
// 连接服务端
int ret = connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
printf("已连接到服务器 %s:%d\n", SERVERIP, SERVERPORT);
参数类别与accept()函数大差不差,唯一区别是这里不用传长度的地址。
4. recv()/send():收发数据
void *read_from_server(void *arg)
void *write_to_server(void *arg)
pthread_t pid_read, pid_write;
// 创建线程处理客户端消息
pthread_create(&pid_read, NULL, read_from_server, (void *)&client_sockfd);
pthread_create(&pid_write, NULL, write_to_server, (void *)&client_sockfd);
// 等待线程结束
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
两个线程函数与服务端基本是一致的,这里就不写了,改一下参数和表述就可以了。
5. close():关闭套接字
close(client_sockfd);
最后关闭客户端的套接字,客户端的代码就编写完成了。
运行结果
编写完server.c和client.c后,通过makefile生成可执行文件,打开Linux终端运行两个文件(我们本次使用的是同一个电脑进行网络通信,如果需要实现远程通信,输入ifconfig查看电脑的IP地址,并修改代码中的SERVERIP)。
运行时需要注意的是,先运行server打开服务端,再运行client连接服务端。

如下图所示,双方都可以收发数据。

扩展:多客户端连接
客户端代码不改变,只需修改服务端代码:
while(1)
{
pthread_t pid_read, pid_write;
socklen_t client_addr_len = sizeof(client_addr);
int client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_addr,&client_addr_len);
handle_error("accept",client_sockfd);
printf("客户端已连接: %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
int *pclient_sockfd = malloc(sizeof(int));
*pclient_sockfd = client_sockfd;
//创建线程处理客户端连接
pthread_create(&pid_read,NULL,read_from_client,pclient_sockfd);
pthread_create(&pid_write,NULL,write_to_client,pclient_sockfd);
//分离线程,避免僵尸线程
pthread_detach(pid_read);
pthread_detach(pid_write);
}
加入while(1)循环,把需要用到的参数定义在循环中,这样就可以实现多客户端连接到同一个服务端的功能了。
感谢阅读!如有侵权,请联系作者



