Socket
Table of Contents
摘录自 → http://c.biancheng.net/socket/
socket 是“套接字”的意思,学习 socket 编程,也就是学习计算机之间如何通信,并用编程语言来实现它。
socket 通信技术 就是两台联网的计算机之间 交换数据的技术 ,这就是 socket 的全部内容了吗?是的!
Socket 是什么
网络编程就是编写程序使两台联网的计算机相互交换数据, 这就是全部内容了!
那么,两台计算机之间用什么传输数据呢?
首先需要物理连接。如今大部分计算机都已经连接到互联网,因此不用担心这一点。在此基础上,只需要考虑如何编写数据传输程序。因为 操作系统已经提供了 socket ,所以即使对网络数据传输的原理不太熟悉,也能通过 socket 来编程。
那么什么到底是 socket 呢?

socket 的愿意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。
socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL ,向服务器发起请求,服务器分析接收到的 URL ,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
1. UNIX/Linux 中的 socket 是什么?
在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
是的,在 UNIX/Linux 中,一切都是文件!
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID ,这个 ID 就是一个整数,被称为 文件描述符 (File Descriptor),例如:
- 通常用
0
来表示标准输入文件(stdin
),它对应的硬件设备就是键盘; - 通常用
1
来表示标准输出文件(stdout
),它对应的硬件就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。
我们可以通过 socket()
函数来创建一个网络连接,或者说打开一个网络文件, socket()
返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
- 用
read()
读取从远程计算机传来的数据; - 用
write()
向远程计算机写入数据。
你看,只要用 socket()
创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!
2. Windows 系统中的 socket 是什么?
Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。
与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了。
Socket 原理1
Socket 在哪儿?如何使用它?
TCP/IP(Transimission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网设计的。UDP(User Data Protocol,用户数据报协议)是与 TCP 相对应的协议,它是属于 TCP/IP 协议族中的一种。

Figure: TCP/IP 协议族中的协议关系
咦?Socket 在哪儿呢??? 来张图看看吧。

哦!原来 Socket 在这里!!!
Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。
在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。
如何使用它们呢?
前人已经给我们做了好多的事了,网络间的通信也就简单了许多,但毕竟还是有挺多工作要做的。以前听到 Socket 编程,觉得它是比较高深的编程知识,但是只要弄清Socket编程的工作原理,神秘的面纱也就揭开了。

先从服务器说起。服务器端先初始化 socket ,然后与端口绑定( bind
),对端口进行监听( listen
),调用 accept
阻塞,等待客户端连接。
在这时,如果有个客户端初始化一个 socket ,然后连接服务器( connect
),如果连接成功,客户端与服务器端的连接就建立了。
客户端发送数据请求,服务器端接收并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
网络中进程之间是如何通信的呢?如,使用浏览器浏览网页时,浏览器的进程是如何通信的?使用 QQ 聊天时,QQ 进程是如何与服务器或你的好友所在的 QQ 进程通信的?
这些都得依靠 socket !
网络中进程之间如何通信
本地的进程间通信(IPC,Inter-Process Communication,进程间通信)有很多种方式,可以总结为下面 4 类:
- 消息传递(管道、FIFO、消息队列);
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量);
- 共享内存(匿名的和具名的);
- 远程过程调用(Solaris 门和 Sun PRC)。
这是本地进程之间的通信,那么网络进程之间是如何通信的呢?
首要解决的问题是如何唯一标识一个进程,否则通信无人谈起。在本地可以通过进程 PID 来唯一标识一个进程,但是在网络中这是行不通的。
幸运地是,TCP/IP 协议族已经帮我们解决了这个问题,网络层的 “IP 地址” 可以唯一标识网络中的主机,而传输层的的 “协议+端口” 可以唯一标识主机中的应用程序(进程)。如此,利用三元组(IP 地址、协议、端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用 TCP/IP 协议的应用程序通常采用应用编程接口:UNIX BSD 的套接字(socket)和 UNIX System V 的 TLI(已淘汰),来实现网络进程之间的通信。
目前而言,几乎所有的应用程序都是采用 socket ,网络时代下网络中进程通信是无所不在的,所以说“一切皆 socket” 。
Socket 的理解
Socket 起源于 UNIX,而 UNIX/Linux 基本哲学之一就是 “一切皆文件” ,都可以用 “打开 open → 读写 write/read → 关闭 close” 模式来操作。Socket 自然也是该模式的一个实现,socket 也是文件,一些 socket 函数就是对其进行的操作(读/写 IO,打开,关闭)。
Socket 的基本函数
socket() 函数
不管是 Windows 还 Linux ,都使用 socket()
函数来创建套接字。 socket()
在两个平台下的参数是相同的,不同的是返回值。
Linux 中的一切都的文件,每个文件都有一个整数类型的文件描述符;socket 也是一个文件,也有文件描述符。使用 socket()
函数创建套接字以后,返回值就是一个 int 类型的文件描述符。
Windows 会区分 socket 和普通文件,它把 socket 当做一个网络连接来对待,调用 socket()
以后,返回值是 SOCKET 类型,用来表示一个套接字。
1. Linux 下的 socket() 函数
在 Linux 下使用 <sys/socket.h>
头文件中 socket()
函数来创建套接字,原型为:
int socket(int af, int type, int protocol);
其中:
af
为地址族(Address Family
),也就是 IP 地址类型,常用的有AF_INET
和AF_INET6
;
type
为数据传输方式/套接字,常用的有SOCK_STREAM
(流格式套接字/面向连接的套接字)和SOCK_DGRAM
(数据报套接字/无连接的套接字);protocol
表示传输协议,常用的有IPPROTO_TCP
和IPPROTO_UDP
,分别表示 TCP 传输协议和 UDP 传输协议。
有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为会么还需要第三个参数( protocol
)呢?
其实,一般情况下有 af
和 type
两个参数就可以创建套接字了,操作系统会自动推演出协议类型。但是,也有特殊情况,比如有两种不同的协议支持同一种地址类型和数据传输类型。如果不指明使用哪种协议,操作系统是没办法自动推演的。
假如,使用 IPv4 地址,参数 af
的值为 PF_INET
,使用 SOCK_STREAM
传输数据,那么满足这两个条件的协议只有 TCP ,因此可以这样来调用 socket()
函数:
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // IPPROTO_TCP 表示 TCP 协议
这种套接字称为 TCP 套接字。
如果使用 SOCK_DGRAM
传输方式,那么满足这两个条件的协议只有 UDP ,因此可以这样来调用 socket()
函数:
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // IPPROTO_UDP 表示 UDP 协议
这种套接字称为 UDP 套接字。
因为上面两情况都只有一种协议满足条件,故可以将 protocol
的值高为 0
,系统会自动推演出应该使用什么协议,如下:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字 int udp_socket = socket(AF_INET, SOCK_DGRAM , 0); //创建UDP套接字
2. 在 Windows 下创建 socket
Windows 下也使用 socket()
函数来创建套接字,原型为:
SOCKET socket(int af, int type, int protocol);
除了返回值类型不同,其他都是相同的。Windows 不把套接字作为变通文件对待,而是返回 SOCKET 类型的句柄。
bind() 和 connect() 函数
socket()
函数用来创建套接字,确定套接字的各种属性,然后服务器要用 bind()
函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect()
函数建立连接。
listen() 和 accept() 函数
对于服务端程序,使用 bind()
绑定套接字后,还需要使用 listen()
函数让套接字进入被动监听状态,再调用 accept()
函数,就可以随时响应客户端的请求了。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直至缓冲区满了。这个缓冲区,就称为 请求队列 (Request Queue)。
*注: listen()
只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept()
函数。
accept()
返回一个新的套接字来和客户端通信,它会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
send()/recv() 和 write()/read() 函数
在 Linux 和 Windows 平台下,使用不同的函数发送和接收 socket 数据。
1. Linux 下数据的接收和发送
Linux 不区分套接字文件和普通文件,使用 write()
可以向套接字中写入数据,使用 read()
可以从套接字中读取数据。
两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用
write()
向套接字写入数据,客户端就能收到,然后再使用read()
从套接字中读取出来,就完成了一次通信。
2. Windows 数据的接收和发送
Windows 和 Linux 不同,Windows 区分普通文件和套接字,并定义了专门的接收和发送的函数。
从服务器端发送数据使用 send()
函数,在客户端接收数据使用 recv()
函数。
Socket 的类型
这个世界上有很多种套接字(socket),比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix 套接字)、CCITT X.25 地址(X.25 套接字)等。
这里只了解 Internet 套接字,它是最具有代表性的,也是最经典常用的。
根据数据的传输方式,可以将 Internet 套接字分成两种类型(其实更多)。通过 socket()
函数创建连接时,必须告诉它使用哪种数据传输方式。
1. 流格式套接字( SOCK_STREAM
)
流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM
表示。
SOCK_STREAM
是一种可靠的、双向的通信数据流,数据可以准确无误到达另一台计算机,如果损坏或丢失,可以重新发送。
SOCK_STREAM
有以下几个特征:
- 数据在传输过程中不会消失;
- 数据是按照顺序传输的;
- 数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。
可以将 SOCK_STREAM
比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先达到,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。

为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。
TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是学说的“路由”。
什么是“数据的发送和接收不同步”呢?
假设传送带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。
流格式的套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区数量,接收端有可能在缓冲区被填满以后一次地读取,也可能分成好几次读取。
也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。
流格式套接字有什么 实际的应用场景 吗?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。
2. 数据报格式套接字( SOCK_DGRAM
)
数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM
表示。
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
因为数据格式套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。
可以将 SOCK_DGRAM
比喻成高速移动的摩托车快递,它有以下特征:
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是同步的(有的教程也称“存在数据边界”)。
众所周知,速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行。这种方式存在损坏或丢失的风险,而且包裹大小有一定限制。因此,想要传递大量包裹,就得分配发送。
另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以“数据的发送和接收是同步的”;换句话说,接收次数应该和发送次数相同。

总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM
来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
*注意: SOCK_DGRAM
没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
Socket 缓冲区及阻塞模式
在 socket 中,可以使用 write()/send()
函数发送数据,使用 read()/recv()
函数接收数据,下面我们就来看看数据是如何传递的。
Socket 缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send()
并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,因为这些都是 TCP 协议负责的事情了。
TCP 协议独立于 write()/send()
函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv()
函数也是如此,也从输入缓冲区读取数据,而不是直接从网络中读取。

Figure: TCP 套接字的 I/O 缓冲区示意图
这些 I/O 缓冲区特性可整理如下:
- I/O 缓冲区在每个 TCP 套接字中单独存在;
- I/O 缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字丢失输入缓冲区中的数据。
*注:输入输出缓冲区的默认大小一般都是 8K 。
阻塞模式
所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
对于 TCP 套接字,当使用 write()/send()
发送数据时:
(1)首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send()
会被阻塞(暂停执行),直至缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send()
函数继续写入数据。
(2)如果 TCP 协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入, write()/send()
也会被阻塞,直到数据发送完毕缓冲区解锁, write()/send()
都会被唤醒。
(3)如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
(4)直到所有数据被写入缓冲区, write()/send()
才能返回。
当使用 read()/recv()
读取数据时:
(1)首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
(2)如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv()
函数再次读取。
(3)直到读取到数据后 read()/recv()
函数才会返回,否则就一直被阻塞。
TCP套接字默认情况下是阻塞模式,也是最常用的,当然你也可以更改为非阻塞模式。