Linux Socket编程入门:从零跑通第一个TCP通信实例

发布时间:2026/10/25 19:23:57
Linux Socket编程入门:从零跑通第一个TCP通信实例

简介:这份文档面向希望系统掌握 Linux 网络编程的开发者与学习者,围绕 Socket 这一核心接口,梳理从进程通信原理到 TCP 连接管理的完整知识链条。内容涵盖网络中进程如何唯一标识与通信、Socket 的“一切皆文件”设计思想,以及 socket()、bind()、listen()、connect()、accept()、read()/write()、close() 等基本函数的用法,并详解 TCP 三次握手建立连接与四次挥手释放连接的过程,最后配有一个可动手实践的服务端与客户端示例,帮助读者把理论落到代码。资源包为 1 个 docx 文档,约 77KB,篇幅紧凑、结构清晰,适合作为入门梳理或复习提纲。目前已有 241 人学习,适合具备一定 C 语言与操作系统基础、想打通网络编程关键概念的读者参考。

1. 从一通“打不通的电话”说起:Linux Socket 编程到底是什么

你有没有遇到过这种情况:两台电脑明明连着同一个网络,想让它们互相传一句话,结果折腾半天,不是连不上就是收不到?很多新手第一次接触网络编程时,脑子里全是问号——浏览器能打开网页、聊天软件能发消息,这些进程之间到底是怎么“说上话”的?

答案就是 Socket。你可以把它想成一部电话:IP 地址是你家的地址,端口号是门牌号,协议是你们约定的语言(TCP 还是 UDP)。Linux Socket 编程,说白了就是教你在代码里“装一部电话、拨号、说话、挂断”。它是网络编程的地基,网页服务器、聊天工具、文件传输,底层几乎都靠它。这份资源就是带你从零把这部“电话”装起来,适合刚学完 C/C++ 基础、想搞懂网络通信到底怎么跑起来的新手。

2. 为什么值得学:先搞懂 Socket 在网络里站哪个位置

2.1 网络进程通信,靠的是“三元组”而不是 PID

本地两个程序通信,操作系统给每个进程一个 PID,靠它就能找到对方。但到了网络上,PID 只在各自机器里有效,换台机器就完全对不上号。TCP/IP 协议族给出的办法是:用 IP 地址标识主机,用“协议 + 端口”标识主机上的某个应用进程,三者合起来就是一个三元组,能唯一锁定网络中的一次通信。

这就是 Socket 存在的理由。它把这套寻址逻辑封装成一套函数接口,你不需要自己去拼 IP 包头,只要调用几个函数,就能让两个进程隔着网络交换数据。入门阶段你只要记住一句话:Socket 是应用程序和 TCP/IP 协议栈之间的那扇门,数据从这扇门出去,也从这扇门进来。

2.2 “一切皆文件”:Socket 为什么长得像文件操作

Unix/Linux 有个很朴素的设计哲学——一切皆文件。普通文件的操作是“打开 open → 读写 read/write → 关闭 close”,Socket 把这套模式原样搬了过来:socket()相当于打开,read()/write()或send()/recv()相当于读写,close()相当于关闭。区别只在于,普通文件背后是磁盘,Socket 背后是网卡和协议栈。

理解这一点,你后面看那些函数就不会觉得陌生。我第一次学的时候,就是把 Socket 描述符当成一个“文件句柄”来理解的:它就是个整数,你拿着这个整数去读写,内核帮你把数据送到网络另一端。这个类比不严谨,但对入门足够用。

2.3 先分清 TCP 和 UDP:两种“说话方式”选哪个

创建 Socket 时有个参数叫类型,最常用的是SOCK_STREAM和SOCK_DGRAM,分别对应 TCP 和 UDP。TCP 像打电话:先拨通(三次握手),确认对方在,然后按顺序说话,说完挂断,丢包会自动重传。UDP 像寄明信片:写好地址扔出去,不保证对方收到,也不保证顺序,但胜在快、开销小。

新手入门建议从 TCP 开始,因为它的连接建立和释放过程清晰,出错时也更容易定位。等你把 TCP 的“装电话—拨号—通话—挂断”跑通了,再回头看 UDP,会发现它简单得有点“随意”。这份资源的例子用的就是 TCP,跟着走一遍,你对“连接”两个字的理解会具体很多。

3. 动手前的准备:六个核心函数就是全部家当

3.1 服务端和客户端的函数分工,一张表看清

Socket 编程的函数不多,但新手容易搞混“谁该调哪个”。下面这张表按角色分好,你写代码时对照着看就行:

函数服务端客户端作用一句话
socket()要调要调创建 Socket 描述符
bind()要调通常不调绑定 IP 和端口
listen()要调不调把 Socket 设为监听状态
connect()不调要调主动发起连接
accept()要调不调接受连接,返回新描述符
send()/recv()要调要调收发数据
close()要调要调关闭连接

服务端的顺序是socket → bind → listen → accept → recv/send → close,客户端是socket → connect → send/recv → close。这个顺序不是随便定的,每一步都在为下一步做准备,少一步就会在运行时报错。

3.2 socket() 和 bind():装电话和报门牌号

socket()的三个参数分别是协议域、类型和协议。入门阶段你基本固定写AF_INET(IPv4)、SOCK_STREAM(TCP)、0(让系统选默认协议)。它返回一个整数描述符,后面所有操作都靠它。

bind()是把地址“报”给这个 Socket。服务端必须绑定一个明确的 IP 和端口,客户端才知道往哪连。这里有个新手必踩的点:端口号要用htons()转成网络字节序,IP 用htonl(INADDR_ANY)表示监听本机所有网卡。原文里特意强调过,主机字节序和网络字节序不一致曾引发过线上问题,别偷懒省掉转换。

// 服务端创建并绑定 Socket 的核心片段 int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP Socket struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // IPv4 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有本机地址 servaddr.sin_port = htons(6666); // 端口转网络字节序 bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

这段代码里,memset先把结构体清零,避免残留垃圾值;htons和htonl负责字节序转换;bind失败会返回 -1,实际写的时候要判断返回值并打印errno,不然出错你只能干瞪眼。

3.3 listen()、connect()、accept():拨号、接听和转接

listen()把主动 Socket 变成被动监听状态,第二个参数是等待队列长度,入门写 10 就够。connect()是客户端发起连接,参数是服务端地址。accept()是服务端从队列里取出一个连接,返回一个全新的描述符——注意,这个新描述符才是用来和这个客户端通信的,原来的监听描述符继续留着接下一个电话。

这里有个容易绕晕的地方:监听 Socket 和已连接 Socket 是两个东西。一个服务器通常只有一个监听 Socket,但每接受一个客户端就多一个已连接 Socket。原文用“电话总机”和“分机”来类比很贴切,你按这个理解就不会混。

3.4 send()/recv() 和 close():说话与挂断

连接建立后,双方用send()发数据、recv()收数据。recv()返回 0 表示对方关闭了连接,返回小于 0 表示出错,大于 0 是实际收到的字节数。新手常犯的错是收到数据后不判断返回值,直接当字符串用,结果缓冲区里混着旧数据。

close()关闭描述符,但要注意它只是把引用计数减一,真正触发 TCP 断开是在计数归零时。所以服务端处理完一个客户端,记得把accept返回的那个描述符关掉,否则连接会越积越多。

4. 第一个能跑的例子:服务端监听 6666,客户端发消息

4.1 把代码拆成四个文件,先跑通再理解

这份资源给的例子结构很清晰:mysocket.h声明一个封装类,mysocket.cpp实现服务端和客户端的创建与收发,server.cpp和client.cpp分别是两个入口。你新建一个目录,把这四个文件放进去,先别急着改,原样编译跑一遍,看到效果再回头读代码。

编译命令如下,两个可执行文件分别生成:

g++ server.cpp mysocket.cpp -o server g++ client.cpp mysocket.cpp -o client

编译时如果报找不到头文件,检查一下mysocket.h是否和 cpp 文件在同一目录。-o后面是输出文件名,你可以改成自己喜欢的名字,但后面运行时要对上。

4.2 服务端先跑:它会在 accept 那里“等电话”

先执行./server,终端会打印“waiting for client's request”,然后停住。这不是卡死,是accept()在阻塞等待客户端连接。服务端的流程是:创建 Socket、绑定 6666 端口、监听、进入accept循环。收到连接后,它用recv循环读取客户端发来的消息并打印。

// server.cpp 入口,逻辑就三行 mysocket server; if (server.createserver(AF_INET, SOCK_STREAM, 0, 6666)) { server.recvmsg(); // 内部 accept 后循环 recv }

createserver里依次做了socket、bind、listen,任何一步失败都会打印错误并返回 false。这种“封装成类”的写法对新手很友好,你不需要在 main 里堆一长串函数调用,先看入口再看实现,层次清楚。

4.3 客户端再跑:改掉那个 IP,否则连的是别人

客户端代码里有一行需要你手动改:inet_pton(AF_INET, "192.168.1.18", ...)。这个 IP 是示例里写死的服务端地址,你在自己机器上跑,要改成127.0.0.1(本机回环),否则它会去连局域网里那台并不存在的机器,然后报 connect 错误。

// client.cpp 里连接服务端的关键行 inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 本机测试改成回环地址 connect(m_isocket, (struct sockaddr*)&servaddr, sizeof(servaddr));

改完重新编译客户端,先确保服务端还在运行,再执行./client。客户端会进入sedmsg循环,不断发送“msg = 0.”“msg = 1.”这样的消息,服务端那边会逐条打印出来。看到两边都有输出,你的第一个 Socket 程序就算跑通了。

4.4 看到结果之后,回头读这三处代码

跑通只是第一步,真正学到东西的是回头读代码。重点看三处:一是createserver里bind之前的memset和字节序转换,这是最容易漏的;二是recvmsg里accept返回新描述符后,recv用的是新描述符而不是监听描述符;三是客户端sedmsg里send失败后continue而不是退出,这种容错写法在实际项目里很常见。

你还可以做个小实验:把服务端listen的第二个参数改成 1,然后同时开三个客户端,观察连接排队现象。这种“改一个参数看结果”的玩法,比死记函数原型有效得多。

5. 新手避坑指南:这五个坑我替你踩过了

5.1 现象:bind 报“Address already in use”

原因:上一次运行的服务端没正常退出,端口还被占用着,或者你用了TIME_WAIT状态下的端口。解决:先ps找到残留进程杀掉,或者在bind之前给 Socket 设置SO_REUSEADDR选项,让系统允许复用处于等待状态的端口。入门阶段最省事的办法是换个端口号,比如 6667。

5.2 现象:客户端 connect 一直失败或超时

原因:九成是 IP 写错了。示例里的192.168.1.18是别人机器的地址,你本机测试必须用127.0.0.1。另外检查服务端是否真的在运行、端口是否一致。解决:先用ping确认地址可达,再用netstat看服务端端口有没有在监听,两步就能定位。

5.3 现象:服务端收到了乱码或者空字符串

原因:recv收到的数据不保证以\0结尾,你直接printf("%s")就会读到缓冲区里的残留内容。原文代码里在recv后手动补了buff[n] = '\0',这一步不能省。解决:每次recv后按返回长度截断,并且memset清空缓冲区再用。

5.4 现象:程序跑一会儿就卡住不动

原因:accept、recv、connect默认都是阻塞的,没有数据时就会一直等。新手容易在单线程里先recv再send,双方都在等对方说话,就死锁了。解决:入门阶段先保证收发顺序明确,客户端先发、服务端后收;等熟悉了再学非阻塞和多路复用。

5.5 现象:编译报错说某个函数未声明

原因:头文件没包含全。Socket 相关函数分散在<sys/socket.h>、<netinet/in.h>、<arpa/inet.h>里,少一个就报错。解决:按原文mysocket.cpp顶部的 include 列表对照补齐,别凭记忆写。

6. 跑通之后往哪走:三条继续深入的路

第一个例子跑通,说明你已经跨过了“敢动手”这道坎。接下来怎么走,我给你三条具体的路,按难度排好,你挑一条先走就行。

第一条是补基础。把 TCP 三次握手和四次挥手对应到代码里的哪一行搞清楚:connect触发 SYN,accept在第三次握手后返回,close触发 FIN。你可以用tcpdump抓一次本机回环的包,亲眼看看那几个标志位,比看十遍文字描述都管用。这一步花半天,后面所有网络问题你都有底气排查。

第二条是改例子。把现在的“客户端狂发、服务端只收”改成“一问一答”:客户端发一句,服务端回一句,客户端收到再发下一句。这个改动会逼你处理收发顺序和缓冲区边界,是理解“面向连接”最直接的方式。改完再试着让服务端支持两个客户端轮流说话,你会自然碰到阻塞的问题,那时候再去查select或epoll,动机就足了。

第三条是换场景。把 TCP 换成 UDP,用sendto/recvfrom重写一遍,感受一下“不需要连接”是什么体验。然后对比两者在丢包、顺序、开销上的差别,你就能理解为什么视频通话用 UDP、文件传输用 TCP。这个对比做完,面试里问“TCP 和 UDP 区别”你就不用背了。

下面这张表可以当你接下来两周的路线参考:

阶段做什么检验标准
第 1 周抓包看三次握手,对照代码能说出 connect 和 accept 各在第几次握手返回
第 2 周改成一问一答,再加一个客户端两个客户端能交替通信不卡死
第 3 周用 UDP 重写并对比能说清什么场景该选哪种

我自己的血泪经验是:每次改完代码,先别急着加新功能,把当前版本完整跑三遍,确认稳定了再往下走。网络编程的坑很多是“改一处、崩一片”,不给自己留后悔药,调试时间会翻倍。从那以后我每次动 Socket 代码,都强制先跑一遍最小例子确认环境没问题,再动业务逻辑。希望这份入门路径能帮到你,先把第一个 6666 端口跑通,剩下的路自然就展开了。

本文还有配套的精品资源,点击获取