模块一 · 应用层
套接字编程
浏览器和服务器怎样使用操作系统提供的网络服务?这一章从一对收发文字的程序出发,区分应用接口、套接字对象和网络协议。
本章要解决的问题:浏览器和服务器怎样使用操作系统提供的网络服务?这一章从一对收发文字的程序出发,区分应用接口、套接字对象和网络协议。
本章的证据来源:教材§2.7,纸书102–109页,及Python、Microsoft官方接口文档。本轮在本机原始笔记工程找到并阅读了tools/socket-lab.py;脚本未随网站提供,旧稿输出也不是本次重新运行的结果。
先从一个例子理解概念
假设你写一个小程序,发出“hello”,另一个程序回复“收到”。你需要告诉操作系统往哪发、从哪收,但不用亲手生成每一个TCP报文。
协议、接口与对象
协议规定通信双方遵循的规则;接口是程序调用操作系统服务的方法。套接字(socket)是程序使用网络服务的通信对象,程序通过它收发数据。
系统用句柄或描述符引用这个对象,Python还把它包装成对象。不能把套接字本身简单等同于“一个编号”。
地址与端口
IP地址用于网络中的寻址;端口帮助主机把数据交给相应的应用端点。下面只考虑IPv4、TCP、一台服务器和一个客户端,通信双方都在同一台机器上,用回环地址127.0.0.1。
回环地址让数据在本机协议栈中传递,这种演示不能代表真实网线、Wi-Fi或跨互联网连接的性能。
TCP程序:先监听,再接受连接
这是接口流程示意,不是本次运行输出:
服务端:socket() → bind() → listen() → accept() → recv()/send()
客户端:socket() → connect() → send()/recv()| 调用 | 应用请求系统做什么 |
|---|---|
socket() | 创建通信对象,选择地址族与协议类型 |
bind() | 指定本地地址与端口 |
listen() | 将TCP套接字置于被动监听状态 |
connect() | 尝试建立到指定对端的连接 |
accept() | 接受队列中的连接,返回用于通信的新套接字 |
send() / recv() | 提交待发送的字节、读取接收到的字节 |
close() | 释放应用持有的套接字资源,连接关闭依状态与选项处理 |
成功的阻塞TCP connect调用意味着连接已建立;调用也可能失败,非阻塞调用则可能在建立尚未完成时返回。因此不能只说“connect返回就完成了握手”。
TCP握手由协议栈处理。监听后的请求可以在应用调用accept之前完成建立;accept负责让应用取得连接,不能把它当成服务器发送某个握手报文的动作。原脚本也先调用客户端的阻塞connect,再调用服务器accept;代码顺序与“accept执行握手”的解释不符。具体握手与关闭状态在运输层再学。
两种服务端套接字,三个对象
继续同一个客户端的例子:
| 对象 | 在谁那里 | 用途 |
|---|---|---|
| 监听套接字 | 服务端 | 等待新连接 |
| 连接套接字 | 服务端 | 与当前客户端收发字节,由accept返回 |
| 客户端套接字 | 客户端 | 与服务器收发字节 |
这个例子合计三个套接字对象。 accept返回的连接对象就是表中的第二个,不能再加一次。原脚本TCP段实际使用srv、cli与accept返回的conn,并依次关闭这三个对象;“四个”是脚本打印说明的计数错误,旧稿又把它解释成重复创建。
监听套接字继续接收其他连接请求。服务器可以用线程、进程或事件循环同时处理多个连接,并非必须给每条连接创建一个线程。
UDP程序:保留数据报边界
数据报是一次发送的独立消息。UDP没有TCP那样的协议建连过程:
服务端:socket() → bind() → recvfrom()/sendto()
客户端:socket() → sendto()/recvfrom()UDP服务端不使用listen/accept。这个例子中服务端显式绑定一个双方约定的端口;客户端可以让系统分配本地端口。UDP程序也可以用其他方式协商服务端实际地址与端口,不必把“服务端端口必须固定”当成协议要求。
UDP套接字也可以调用connect。 它设置默认对端等本地套接字状态,不执行TCP三次握手;协议无连接不代表接口不能记录对端。
recvfrom返回数据和发送方地址。数据报的源端口来自UDP首部,源IP来自IP层,不能说“UDP首部自带全部地址信息”。TCP提供字节流,不保留应用的一次send对应的一条消息边界。
一次读取能拿到多少
- TCP的recv给出当前可读的一部分字节,上限由缓冲参数指定。应用要自行约定消息边界,例如长度前缀。
- UDP接收针对一个数据报,但接收缓冲过小可能截断数据,或在Windows等实现中报告消息过长错误。保留边界不等于任何大小的缓冲都能收到完整内容。
发送成功与错误回报
send/sendto成功只表明本地系统接受了该次发送请求或相应数量的数据,不保证对端已收到,更不保证对端应用已处理。
Windows上,UDP收到针对先前发送的ICMP端口不可达通知后,某些后续接收操作可报告WSAECONNRESET。能否收到通知、何时通过接口交付,还受协议、系统选项及过滤影响;不能把一次历史观察扩展成“错误只能在下一次操作时出现”的通用规律。
旧稿记录的WinError 10054在本次没有复现。接口定义可直接核对Microsoft文档;真实报文与错误出现的时间则需要原脚本、抓包和运行条件。
端口0:让系统选择本地端口
bind到本地端口0,是请求系统选择可用端口。之后用getsockname读取实际绑定的地址和端口,再告诉需要连接的一方。
系统可用端口范围依系统配置而异。这个接口行为不能证明当前机器必定使用旧稿列出的端口,也不能让客户端自动知道服务器选中了哪个端口。
本章小结
| 结论 | 范围 |
|---|---|
| 套接字是通信对象,程序通过接口使用协议服务 | 区分对象、句柄、接口和协议 |
| TCP服务器区分监听与连接套接字 | 一个客户端的示例合计三个对象 |
| accept取出连接,不等同于执行三次握手 | 应用接口与协议栈行为不同 |
| UDP不使用listen/accept,也可调用connect | connect不建立UDP协议连接 |
| TCP是字节流;UDP保留数据报边界 | UDP仍要考虑缓冲过小 |
| 本地发送成功不能证明对端交付 | TCP和UDP都适用 |
| 原始脚本已阅读,运行结果未在本次复现 | 脚本存在于原始笔记工程,网站未提供附件 |
自测题
7 题 · 先自己答,再展开对照
协议、套接字对象与套接字接口是什么关系?
协议规定通信规则;套接字是应用使用网络服务的对象;接口调用让应用操作这个对象。句柄或描述符只是引用对象的方式。一个监听TCP服务器与一个客户端通信,典型示例里有几个套接字对象?
三个:服务端监听套接字、accept返回的服务端连接套接字、客户端套接字。不能把accept返回的对象重复加一次。accept是否等于服务器执行三次握手?
不等于。握手由协议栈处理,连接可在应用accept之前建立;accept让应用取得队列中的连接。UDP能调用connect吗?它会建立TCP那样的连接吗?
可以,它设置默认对端等本地状态,不执行TCP三次握手。UDP协议的无连接与接口记录对端是不同概念。send返回成功,能证明对端应用已经收到数据吗?
不能,只能说明本地系统接受了发送请求或相应数量的数据。协议送达和应用处理都不能由这次返回值直接推出。bind时端口写0是什么意思?客户端怎样知道实际端口?
请求系统选择可用本地端口。程序用getsockname读出实际端口,再通过约定的方式告知客户端。TCP的recv能一次收到一整条应用消息吗?UDP缓冲太小又会怎样?
TCP不保留消息边界,可能分次返回,应用须自行定界。UDP针对一个数据报接收,但缓冲过小可能截断或报错,不能保证总能取到完整数据。
参考资料与适用范围
- 《计算机网络:自顶向下方法(原书第8版)》§2.7.1–2.7.2,纸书102–109页(PDF121–128页),图2-27、图2-28:UDP/TCP应用接口与套接字分工。
- Python官方:socket对象、accept、connect、recv与recvfrom
- Microsoft Learn:accept的队列及返回句柄
- Microsoft Learn:connect用于无连接套接字的语义
- Microsoft Learn:recvfrom的截断、WSAEMSGSIZE与WSAECONNRESET
- RFC 9293 §3.9:TCP用户接口
修订核对日期:2026-10-13。平台差异按官方文档理解;原始socket-lab脚本已作静态核对,旧稿历史观察没有在本次重新实测,网站也未打包该脚本。