C语言实现网络客户端与服务器,核心就是用好socket套接字编程接口,配合TCP/IP协议栈完成数据收发。无论是Linux还是Windows,流程都围绕socket()、bind()、listen()、accept()、connect()这几个函数展开,本文直接讲透完整步骤和代码骨架,帮你少走弯路。
c语言socket编程客户端服务器核心流程
先明确一个概念:客户端主动发起连接,服务器被动等待连接,两者都依赖socket作为通信端点,差别只在后续调用的函数顺序,下面按角色拆解。
服务器端四步走
第一步创建套接字,调用socket(AF_INET, SOCK_STREAM, 0),第一个参数指定IPv4地址族,第二个参数选SOCK_STREAM表示TCP流式协议,第三个参数填0让内核自动选TCP,这一步返回一个整数文件描述符,后续所有操作都靠它。
第二步绑定地址和端口。bind()函数把socket和本地IP、端口绑在一起,这里有个常见坑:端口号需要调用htons()把主机字节序转成网络字节序,IP地址用inet_pton()把点分十进制字符串转成二进制结构体,不处理字节序,连接会直接失败。
第三步监听等待连接。listen(sockfd, backlog)进入监听状态,backlog表示内核维护的连接队列长度,一般设为5到16,此时客户端就算发来连接请求,也只是进入队列,还没被真正处理。
第四步接受连接。accept()从队列里取出一个已完成握手的连接,返回一个新的socket文件描述符,这一步是阻塞操作,没有客户端连进来时,程序会卡在这一行,后续收发数据都用新描述符,原来的监听描述符继续等待其他连接,服务器处理完客户端请求,用close()关闭连接。
客户端三步就位
客户端的套路更短,同样先socket()创建套接字,接着不绑定本地地址,而是直接调connect()连接服务器IP和端口,connect内部会由内核自动分配临时端口,并完成TCP三次握手,握手成功,就能用send()和recv()收发数据。
这里要提醒一句,connect失败时返回-1,常见错误码是ECONNREFUSED,表示目标端口没有服务在监听,排查顺序:先ping通不通、再telnet通不通、最后查防火墙。
最小可运行的TCP通信骨架
服务器端核心代码逻辑如下:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr)&addr, sizeof(addr));
listen(sockfd, 8);
int client_fd = accept(sockfd, NULL, NULL);
char buf[1024] = {0};
recv(client_fd, buf, sizeof(buf), 0);
客户端核心代码如下:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server = {0};
server.sin_family = AF_INET;
server.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);
connect(sockfd, (struct sockaddr)&server, sizeof(server));
send(sockfd, "hello", 5, 0);
先启动服务器,再运行客户端,客户端发一个hello,服务器终端就能打印出来,这套骨架覆盖了80%的入门场景。
c语言写网络编程用什么库:标准库还是第三方
很多新手被网上五花八门的封装库带偏,其实纯C语言写网络编程,不需要任何第三方库,Linux下socket接口就是libc标准库的一部分,编译时不用加额外链接参数,直接gcc server.c -o server就能出可执行文件。
常用辅助库盘点
- libevent:事件驱动库,把select、epoll、kqueue封成统一接口,服务器要同时处理上千个连接时,用它比手写epoll方便不少。
- libuv:Node.js底层的异步库,C语言封装得比较现代,跨平台做的很好。
- pthread:多线程场景下的标配,不是网络库但经常一起用。
入门阶段,先把裸socket练熟,换个角度看,socket API本身就是一套设计良好的抽象层,业务逻辑不复杂时硬拆成第三方库反而增加学习成本。
裸socket和事件库的取舍
客户端程序、内部工具、并发量小的游戏服务器直接裸写,高并发网关、IM长连接服务,优先考虑libevent或libuv,行业共识认为,事件驱动库能把epoll的频繁轮询逻辑封装好,让开发者专注业务,不必纠结内核API差异。
linux c网络编程入门从哪个环节入手
先别急着写代码,推荐按这套顺序推进,第一步把TCP三次握手和四次挥手的过程图画明白,连接断开时服务器会进入TIME_WAIT状态,这是面试和实际排查的高频点,初学者经常在这里卡住。
第二步动手写上一篇那个骨架程序,本地回环地址0.0.1测通后,再改成局域网IP,用两台电脑互发数据,记住一点,先跑通再优化,绝大多数性能问题在代码量低于500行时都是伪问题。
第三步用抓包工具验证流程,终端执行tcpdump -i any port 8080 -nn,能看到SYN、SYN-ACK、ACK三次握手的完整报文,看到这三个包,说明你的socket调用完全正确,这一步帮你把抽象协议和实际代码对应起来,比看十篇文章都管用。
多客户端处理的三个层次
写服务器最容易踩的坑是:accept()一次就只会阻塞在处理第一个连接上,第二个客户端来的时候直接连不上,解决方式有三个层次:
单线程阻塞模型,代码简单,循环调用accept然后处理数据,同一时刻只能服务一个客户端,适合学习,不适合生产环境。
多线程模型,每来一个客户端就开一个线程,创建线程用pthread_create,数据收发在线程函数里做,这个方案撑住几百个连接没问题,但线程间切换开销不小。
I/O多路复用模型,用select()或epoll()同时监控多个描述符,有事件才处理,单线程支撑上万连接很轻松,其中epoll是Linux平台的主流选择,写服务器时优先考虑它。
阻塞与非阻塞两种工作方式
socket默认是阻塞模式,即recv()函数在没有数据的时候卡住当前线程,非阻塞模式靠fcntl()或ioctl()开启,函数立刻返回,配合select()、epoll()一起用,能极大提升程序响应速度。
初学者先用默认的阻塞模式,把流程跑通,之后进阶时再切到非阻塞加epoll的经典组合,这也是主流服务器的标准写法。
c语言网络编程的三个高频坑
数据粘包问题,TCP是字节流协议,没有消息边界,比如连续发送两次”hello”,对端可能一次性收到”hellohello”,解决方式有几种:消息固定长度、消息头和体分离、添加结束标记词,固定长度最简单,实现自定义协议最通用。
字节序混乱,整数如果大于一个字节,高字节和低字节的存储顺序在不同架构上有差异,网络传输统一用大端序,发送之前用htonl()、htons()转换,字符串数据不受影响,整数必须先转再发。
连接不关闭,客户端断开连接时,服务器不调用close()就继续循环,会导致资源泄漏,连接一多,系统报”Too many open files”,机器卡住,服务器每处理完一次请求,记得把accept返回的套接字关掉。
c语言tcp客户端与服务器高频疑问解答
问:客户端要先启动还是服务器先启动?
必须服务器先启动,因为客户端connect需要知道目标IP和端口,如果服务端没运行,connect会直接返回-1,如果实在要客户端先启动,需要引入服务发现机制,比如客户端定期重试,这是生产环境才需要考虑的方案。
问:如何在windows下实现同样的功能?
Windows下有几个差异点,头文件改成winsock2.h,第一步调用WSAStartup()初始化Winsock库,socket函数名几乎一致,只是连接关闭用closesocket()而不是close,代码本身多用条件编译#ifdef _WIN32来做跨平台适配,流程和Linux完全一致。
问:accept失败后服务器会不会崩?
多数情况下不会,accept返回-1后,服务器代码只要不拿这个负描述符去收发数据就不会段错误,常见处理方式:判断失败后continue跳过一次循环,真正的风险点在于连接数满、描述符耗尽导致的listen失败,这类问题暴露在生产环境的高并发压力下。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/769740.html




