为什么要讲socket呢?
其实也没什么特别的原因,主要是自己想了解一下网络通信这块的东西,然后一直觉得socket高深难懂,所以决定挑战一下自己,做一个简单的学习了解。
以前因为项目原因接触过socket,但是当时是使用的socket.io的这个框架,对于socket的底层是如何实现的,以及框架是如何封装的并没有进行深入的研究。
所以这次就抛砖引玉简单的讲一下我对socket的理解,非常欢迎各位大佬多多指教。
什么是socket?
字面上socket又成为“套接字”
实际上:网络上 的 两个程序 通过一个双向的通信连接 实现数据的交换,这个连接的一端称为一个socket
客户端和服务端的socket建立一个连接,通过两端建立的这个通信管道进行网络的请求和响应。socket可以理解成通信管道(隧道)的两个端口,一个入口,一个出口

网络通信的三要素
-
IP地址(网络上主机设备的唯一标识)
- 用来寻找对应服务器的主机
-
端口号(定位程序)
- 用于标示进程的逻辑地址,不同进程的有不同的端口号
- 相当于当前服务器(当前电脑)上对应的 web应用程序
- 有效端口:0 ~ 65535,其中0 ~ 1024由系统使用或者称作保留端口,开发中建议使用1024以上的端口
-
传输协议(使用什么样的方式进行交互,通信规则)
- 常见协议:TCP、UDP
传输协议
TCP(传输控制协议)
- 需要建立连接,形成传输数据的通道
- 在连接中进行大数据传输(数据的大小不受限制)
- 通过3次握手完成连接,是可靠协议,安全送达
- 必须建立连接,效率会稍低
UDP(用户数据报协议)
- 将 数据 及 源和目的 封装成数据包中,不需要建立连接
- 每个数据报的大小限制在64K之内
- 因为无需连接,因此是不可靠协议
- 不需要建立连接,速度快
套接字
套接字的类型有很多种,比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。涉及到网络请求方面的主要指的都是Internet套接字
根据数据的传输方式,可以将Internet套接字分成两种类型
流格式套接字(SOCK_STREAM)也叫“面向连接的套接字”
-
SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
-
流格式套接字有自己的纠错机制
-
SOCK_STREAM 有以下几个特征:
- 数据在传输过程中不会消失;
- 数据是按照顺序传输的;
- 数据的发送和接收不是同步的(有的也称“不存在数据边界”)
-
可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(也就是不断网),就可以保证数据不会丢失;同时,比较晚传送的数据不会先到达,比较早传送的数据不会晚到达,保证了数据的传递是按照顺序传递的。

-
流式套接字使用的是传输控制协议(TCP),进而保证了数据可以 准确无误的顺序到达,并且接收者不需要和发送者保持相同的节奏接收数据。
- 流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。
- 也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。
-
应用场景:HTTP协议就是基于面向连接的套接字(TCP服务),数据信息必须要准确无误的进行传输
数据报格式套接字(SOCK_DGRAM)也叫“无连接的套接字”
-
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
-
SOCK_DGRAM 有以下几个特征:
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是同步的(有的教程也称“存在数据边界”)。
-
可以将SOCK_DGRAM理解为快递行业,用货车发往同一地点的两件包裹无需保证顺序,只要以最快的速度送到客户手中即可。这种方式存在损坏或者丢失的风险,而且对快递包裹的大小也是有一定限制的。所以当传递大量包裹的时候就需要分批发送。

-
用两辆车分别发送的两件包裹,接收者也需要分两次接收,所以数据的发送和接收必须是同步的。(也可以理解为:接收次数和发送次数是相同的)
-
总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字,使用的是UDP协议。
-
应用场景:QQ视频聊天 & QQ语音 都是使用SOCK_DGRAM来进行数据传输的。
面向连接和无连接的套接字的理解

面向连接的套接字
- 对于面向连接的套接字则是在通信之前先确定好一条路径,没有特殊情况,以后固定走这条路径进行数据的传递,当然如果这条路径出问题或者被破坏的话则会在发送数据之前重新建立新的路径。
- 为了确保数据的准确性以及发送顺序,需要在收到数据包的确认消息之后才进行下一次数据的发送,否则进行数据的重发。
- 非常可靠,确保万无一失
无连接的套接字
- 对于无连接的套接字,每个数据包可以选择不同的路径,当然也可以选择相同的路径,无论中途走那条通道或者经历了什么,最终到达即可,无论先后以及最终到达是否成功
- 尽最大努力交付 原则
Socket通信流程
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open -> 读写write/read -> 关闭close”模式来操作。也可以理解为socket就是该模式的一个实现,socket即是一种特殊的文件
流程图


接口简介
-
socket(): 创建socket套接字
-
bind(): 将套接字和IP、端口绑定,通常由服务端调用
-
listen(): TCP专用,开启监听状态,等待用户发起请求,套接字一直处于“睡眠”中,直到客户端发起请求才会被“唤醒”
-
accept(): TCP专用,服务器等待客户端连接请求,一般是阻塞态(暂停运行),直到客户端发起请求
-
connect(): TCP专用,客户端向服务器主动发起连接请求,直到服务器传回数据后,connect() 才运行结束
-
send(): TCP专用,发送数据
-
recv(): TCP专用,接收数据
-
sendto(): UDP专用,发送数据到指定的IP地址和端口,数据信息包含目标地址
-
recvfrom(): UDP专用,接收数据,返回数据远端的IP地址和端口,接收数据请求的时候将目标地址信息保存
-
shutdown(): TCP专用,优雅的断开流连接,断开输入流 断开输出流 同时断开I/O流,会等缓冲区的数据发送完毕之后再断开。用来关闭连接,而不是套接字,所以需要在调用之后再调一次close(),才能将套接字从内存中清除
-
closesocket(): 关闭socket套接字,不仅会关闭服务端的socket,还会通知客户端连接已断开,客户端也会清理 socket 相关资源,该操作会丢失输出缓冲区中的数据
不同协议下socket通信的注意点
基于TCP的socket通信
-
TCP的服务端与客户端必须一对一的建立连接才可以进行数据的通信
-
在TCP中,套接字是一对一的关系,如果要向10个客户端提供服务的话,那么除了负责监听的套接字之外,还需要创建10个套接字
-
创建好TCP套接字之后,传输数据时无需添加地址信息,因为TCP的套接字与对方套接字相连接,知道数据的目标地址信息
基于UDP的socket通信
-
UDP中的服务器端和客户端没有连接,无需在连接状态下交换数据
-
UDP 套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址
-
在UDP中,不管是服务端还是客户端都只需要1个套接字即可
-
UDP不存在严格的服务端和客户端的区分,只是因为其提供服务而称为服务端
问题探讨
http 的长短连接 & 长短轮询
长连接 & 短连接
首先需要强调一点:HTTP协议是基于请求/响应模式的,因此只要服务端给了响应,本次HTTP连接就结束了。 也可以理解为是本次HTTP请求就结束了,根本没有长连接 短连接这一说
对于网络上说的HTTP分为长连接和短连接,其实是指TCP连接。TCP连接是一个双向的通道,它是可以保持一段时间不关闭的,因此TCP连接才是真正有长连接和短连接之分的
对于HTTP连接的说法 其实是HTTP请求和HTTP响应更为准确,所以 长连接是指的TCP连接,而不是HTTP连接
怎样算是把HTTP变成长连接?
首先要明白 长连接意味着连接会被重复使用,是指TCP的连接通道被重复使用,并不是HTTP请求的复用,是多个HTTP请求可以复用同一个TCP连接,这样节省了很多TCP连接建立和断开的握手消耗。
HTTP1.0协议不支持长连接,从HTTP1.1协议以后默认是长连接,我们可以看到平时的Web应用的HTTP头部Connection也确实是keep-alive,但是需要服务端和客户端都设置才算是长连接。
长连接也并不是永久连接的,如果一段时间内(具体的时间长短,是可以在header当中进行设置的,也就是所谓的超时时间),这个连接没有HTTP请求发出的话,那么这个长连接就会被断掉。
长连接优势: 假设打开一个网页,里面肯定包含很多CSS、JS等一系列资源,如果是短连接的话(每次都要重新建立TCP连接),所以一个网页的打开需要耗费几个甚至几十个的TCP连接。如果是长连接的话,那么这么多次HTTP请求(这些请求包括请求网页内容,CSS文件,JS文件,图片等等),其实使用的都是同一个TCP连接,很显然这样可以节省很多的消耗。
长轮询 & 短轮询
假设一个场景:淘宝界面显示库存的剩余个数,客户端写一个死循环,不停的去请求服务端的数据
短轮询
- 服务端立刻返回请求的结果
长轮询
- 当服务端收到客户端的请求的时候,并不是立刻去返回结果,而是检查一下请求的数据有没变化,检测到有变化则立即返回,否则一直等到超时为止。
对于客户端来说,长轮询和短轮询是一样的,就是不停的去请求;对于服务端来说,短轮询的情况下服务端每次请求不管有没有变化都会立即返回结果信息,而长轮询的情况下是有变化才返回结果信息,没变化的话则不会返回信息,直到超时为止
长短轮询和长短连接的区别
-
1.决定方式
- 一个TCP连接是否为长连接,是通过设置HTTP的Connection Header来决定的,而且是需要两边都设置才有效
- 一种轮询方式是否为长轮询,是根据服务端的处理方式来决定的,与客户端没有关系
-
2.实现方式
- 连接的长短是通过协议来规定来实现的
- 轮询的长短,是服务器通过编程的方式手动挂起请求来实现的
Socket 的短连接 长连接
Socket的长短连接的差别:整个客户端和服务端的请求是通过一个socket还是多个socket进行的
- 长连接是整个通信过程中,服务端和客户端只用一个socket对象,长期保持socket的连接
- 短连接是每次请求的时候都新建一个socket,处理完一个请求就直接关闭socket
关于连接的关闭问题
Socket连接中 断开流 其实就是断开连接了,因为Socket本来就是依靠流进行通信的
在关闭连接的时候 一定要显示关闭socket,而不是通过调用shutDown方法关闭某个流,这样容易造成内存泄漏
顺序是先关流再关socket
如何实现长连接?
首先要知道:要实现长连接,一般需要发送结束标记符号来告诉客户端 - 服务端的某段消息已经发送完毕,否则客户端会一直阻塞在read方法
所以 客户端需要自己主动退出读取的动作,为了防止客户端一直阻塞在read()方法处
我们可以在服务端每发送完一段消息并且刷新前就进行一个写入结束符号的标志,当客户端解析到结束符号时,就可直接退出read的循环读取操作,避免一直阻塞。
Socket如何保持长连接?
方法1:应用层自己实现心跳包
节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉,所以要保持长连接,需要保活,需要发送心跳包,防止长时间没有数据交互,导致连接被断掉
客户端和服务端制定一个通信协议,每隔一定时间(一般15秒左右),由一方发起,向对方发送协议包;对方收到这个包后,按指定好的通信协议回一个。若没收到回复,则判断网络出现问题,服务器可及时的断开连接,客户端也可以及时重连。
总的来说:心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒
方法2:TCP的KeepAlive保活机制
通过TCP协议层发送KeepAlive包。这个方法只需设置好你使用的TCP的KeepAlive项就好,其他的操作系统会帮你完成。操作系统会按时发送KeepAlive包,一发现网络异常,马上断开。
开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启KeepAlive功能,另一方面,KeepAlive设置不合理时可能会因为短暂的网络波动而断开现在的TCP连接。并且,默认的KeepAlive超时需要7,200,000 MilliSeconds, 即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数。
TCP的 SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。
TCP粘包问题
客户端同一时间发送几条数据,而服务端接收的是这几条数据合在一起的一条数据,经过TCP的传输,三条数据被合并成一条了,这就是数据的粘包问题。 也称数据的无边界性,read()/recv()函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。
解决办法
封包的时候给每个包的数据加一个标记,来标明数据的长度和类型(类型显然是需要的,我们需要知道它是文本、图片、还是录音等等,来用正确的方式处理这个数据)。
拆包的时候,先获取到我们给每个包的标记,然后根据标记的数据长度,去获取数据。最后再根据标记的类型去处理数据。(文字输出、图片展示、录音播放等等)