网络编程基础:socket、bind、listen、accept,TCP客户端服务器
说实话,很多嵌入式工程师一听到「网络编程」就头大。觉得那是搞Linux后台的人的事。我当年也这么想,直到有一次做智能网关项目,发现设备之间必须走TCP通信……嗯,硬着头皮啃下来后,发现其实就那么几个API来回倒腾。
今天咱们就把TCP网络编程这层窗户纸捅破。你只要搞懂四个函数——socket、bind、listen、accept,再加上客户端的connect,就能写出能跑起来的网络程序。
1. 先搞清楚:TCP通信到底长啥样?
说白了,TCP就像打电话。服务器得先装个电话机(socket),然后绑定一个号码(bind),接着把电话线接好等着(listen),有人打进来就接听(accept)。客户端呢?拿起电话(socket),直接拨号(connect),接通了就开始聊天(send/recv)。
我画了张图,你看一眼就明白了:
2. 四个核心函数,一个一个说
2.1 socket() — 创建通信端点
这个函数就是跟内核说:「嘿,给我一个网络通信的句柄。」返回的是一个文件描述符,你可以把它当成一个特殊的文件来读写。
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET: IPv4协议
// SOCK_STREAM: 流式套接字,对应TCP
// 第三个参数0:自动选择协议
我在项目中遇到过一个问题:socket创建成功后,忘了检查返回值。结果后面bind的时候一直报错,查了半天才发现是socket创建失败。所以我的习惯是——每个系统调用后都检查返回值。
2.2 bind() — 绑定地址和端口
bind说白了就是把你的套接字和一个具体的IP地址、端口号绑定在一起。服务器必须调用bind,客户端一般不用。
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
serv_addr.sin_port = htons(8888); // 端口号
if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("bind failed");
exit(1);
}
这里有个坑:htonl和htons是干嘛的?说白了就是字节序转换。网络字节序是大端,而x86是小端。你不转换的话,端口号就错了。我曾经见过一个同事,bind端口8080,结果实际绑到了32815,就是因为忘了htons。
2.3 listen() — 开始监听
bind之后,套接字还处于「未监听」状态。listen就是告诉内核:「我开始接客了,有连接请求就排队等着。」
if (listen(sockfd, 5) < 0) {
perror("listen failed");
exit(1);
}
// 第二个参数5:最大等待队列长度
第二个参数叫backlog,表示内核为这个套接字排队的最大连接数。你想想看,如果同时有10个客户端连过来,而你只设了3,那后面7个就会被拒绝。我一般设5~10,具体看你的应用场景。
2.4 accept() — 接受连接
accept是个阻塞函数。它一直等着,直到有客户端连上来,然后返回一个新的套接字——这个新套接字专门用来和这个客户端通信。
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int connfd = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);
if (connfd < 0) {
perror("accept failed");
exit(1);
}
printf("新客户端连接: %s:%d\n",
inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port));
3. 客户端:connect() 就够了
客户端比服务器简单得多。创建socket后,直接connect到服务器的IP和端口就行。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.1.100", &serv_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect failed");
exit(1);
}
// 连接成功,开始通信...
connect也是阻塞的。它会触发TCP三次握手,握手成功才返回。如果服务器没启动或者网络不通,connect会超时返回-1。我建议你设一个超时时间,别让客户端卡死在那。
4. 完整示例:回声服务器
光说不练假把式。下面是一个完整的TCP回声服务器——客户端发什么,服务器就回什么。
// server.c - TCP回声服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int listenfd, connfd;
struct sockaddr_in serv_addr, cli_addr;
socklen_t cli_len;
char buf[1024];
int n;
// 1. 创建套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket");
exit(1);
}
// 2. 绑定地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(8888);
if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("bind");
exit(1);
}
// 3. 开始监听
if (listen(listenfd, 5) < 0) {
perror("listen");
exit(1);
}
printf("服务器已启动,监听端口8888...\n");
// 4. 接受连接
cli_len = sizeof(cli_addr);
connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
if (connfd < 0) {
perror("accept");
exit(1);
}
printf("客户端已连接\n");
// 5. 通信循环
while ((n = read(connfd, buf, sizeof(buf))) > 0) {
write(connfd, buf, n); // 原样返回
}
close(connfd);
close(listenfd);
return 0;
}
// client.c - TCP回声客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in serv_addr;
char buf[1024];
int n;
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 连接服务器
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect");
exit(1);
}
printf("已连接到服务器\n");
// 3. 发送数据
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(sockfd, buf, strlen(buf));
n = read(sockfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
5. 避坑指南
我这些年做网络编程,踩过的坑不少。挑几个典型的说说:
- 地址已使用(Address already in use):服务器崩溃后,端口还处于TIME_WAIT状态。解决方案是用
setsockopt设置SO_REUSEADDR选项。 - 阻塞问题:read和write默认是阻塞的。如果对方不发数据,read会一直等。我建议用非阻塞模式或者select/poll/epoll。
- 粘包问题:TCP是流式协议,没有消息边界。你发两次数据,对方可能一次就收完了。解决办法是自定义应用层协议,比如加个长度头。
- 字节序:永远记得用htonl/htons/ntohl/ntohs。不同平台字节序不一样,别偷懒。
#define _GNU_SOURCE,这样可以用一些扩展功能。另外,调试时用strace跟踪系统调用,能快速定位问题。
6. 总结一下
TCP网络编程的核心就这几个API。服务器:socket → bind → listen → accept → read/write。客户端:socket → connect → read/write。你把这个流程刻在脑子里,以后遇到任何网络应用,底层都是这套东西。
嗯,最后提醒一句:别光看代码,自己动手敲一遍。我当年就是照着书抄了三遍,才真正理解每个参数的含义。网络编程这东西,纸上谈兵没用,得真刀真枪地跑起来。
公众号:蓝海资料掘金营,微信deep3321