前言
不明白 Socket 是什么的主要原因其实就是没有实际的网络编程经验,就没有在代码里用过 Socket,背来背去还是脑袋一片浆糊,很正常,看完这篇文章肯定就清楚了(狗头)
TCP 四元组
要说 Socket,那当然不能绕过 TCP 了,各位不妨先来思考下如何确定一个 TCP 连接?
以小黑和小白为例,他们分别位于不同的小区,小黑找小白玩,需要知道小白的小区和门牌号,也就是说,小区 + 门牌号就是小白家的入口,知道了这个入口,小黑就能找到小白。反之也是同样的。
小区类比于 IP 地址,门牌号类比于端口号,IP 地址 + 端口号(小区 + 门牌号) 就能唯一确定一个程序。光有小区不行,光有门牌号也不行,所以这就是为什么说网络层负责建立主机到主机的通信(IP 地址),传输层负责建立端口到端口的通信(端口号)了。
这个很好记忆,你上线一个网站的时候,如果没有绑定域名的话,那么就只能通过 IP 地址 + 端口号(默认是 80,浏览器上不显示)访问。
总结下就是,TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 目的 IP 地址
- 目的端口
- 源 IP 地址
- 源端口
其中:
- IP 地址(源地址和目的地址,32位)是在 IP 头部中
- 端口号(源端口和目的端口,16位)是在 TCP 头部中
Socket
掌握了四元组这个基本概念,我们再来解释 Socket。
上文说过,小区 + 门牌号是住宅的入口,IP 地址 + 端口号是一个程序的入口,这个入口就是 Socket,那么服务端和客户端之间想要进行通信,只要互相暴露出自己的入口(Socket),就能够找到彼此了。
更严谨来说,Socket 封装了基本的通信功能,是 TCP/IP 协议的基本操作单元。
以 Java 中的 Socket 类为例,服务端和客户端首先都需要调用构造函数创建 Socket 暴露自己的入口(绑定 IP 地址和端口,也可以调用 bind 方法进行绑定)
光暴露了入口还不行,你还得竖起耳朵听,不然别人来敲门你听不见那也没法通信啊,所以接下来服务端调用 accept() 方法,该方法将一直等待,直到客户端请求服务端的入口,再就是 TCP 三次握手建立连接的过程了。
服务端 Socket 创建一般使用 ServerSocket 类,该类提供了非常重要的 accept(建立连接) :
那客户端是如何请求服务端的入口的呢?也就是是如何发起连接的呢,客户端在创建好 Socket 后,调用 connect(host, port) 函数发起连接,该函数需要指明服务端的 IP 地址和端口号。
所以说,TCP 三次握手其实是发生在客户端 connect 和服务端 accept 两个函数之间。握手完了就可以通过 read() 和 write() 来通信啦。这里需要重点注意的是:监听的 Socket 和真正用来传数据的 Socket 是两个不同的 Socket:
- 一个是 监听 Socket;
- 一个是 已连接 Socket;
看下上述的 ServerSocket.accept 方法就明白了,accept会返回一个 Socket 对象,后续服务端和客户端之间的数据传输都用这个 Socket:
事实上,在三次握手的过程中,内核(Kernel)为每个连接都维护了两个队列:
- TCP 半连接队列:这个队列存储没有完成三次握手的 Socket,此时服务端处于 syn_rcvd 的状态;
- TCP 全连接队列:这个队列存储已经完成了三次握手的 Socket,此时服务端处于 established 状态;
当 TCP 全连接队列不为空后,服务端的 accept() 函数,就会从内核中的 TCP 全连接队列里拿出一个已经完成连接的 Socket 并返回,用于后续服务端和客户端的通信。
总结
综上, 基于 TCP 协议的 Socket 调用过程就结束了,下面由贴心助理 ChatGPT 总结下:
以下全是 ChatGPT 生成的结果,没有一个字是我写的(😂),虽然是我引导了很多轮的结果,但是输入合适的 Promt 并配合上下文 ChatGPT 基本能输出 90% 想要的内容,确实太强了
文字解释:
代码示例:
客户端代码示例
服务器端代码示例
由于我懒得画图,所以决定再让 ChatGPT 帮我生成下,虽然结果不是很行,不过还是能看,不用我费劲画了,舒服!
在上面的流程图中,Socket 客户端和服务端之间的通信过程如下:
- 客户端创建一个 Socket 对象,指定服务端的 IP 地址和端口号,然后调用 connect() 方法发起连接请求。
- 服务端创建一个 ServerSocket 对象,并指定端口号,然后调用 accept() 方法等待客户端连接请求。
- 当客户端的连接请求到达服务端,服务端的 accept() 方法会返回一个新的 Socket 对象,该 Socket 对象代表了客户端和服务端之间的通信连接。
- 客户端可以通过该 Socket 对象的 getOutputStream() 方法获取输出流对象,用于向服务端发送数据;也可以通过该 Socket 对象的 getInputStream() 方法获取输入流对象,用于接收服务端发送的数据。
- 服务端可以通过该 Socket 对象的 getOutputStream() 方法获取输出流对象,用于向客户端发送数据;也可以通过该 Socket 对象的 getInputStream() 方法获取输入流对象,用于接收客户端发送的数据。
- 客户端和服务端可以通过各自的输出流和输入流进行数据的读写操作。
- 当通信完成后,客户端和服务端都需要调用该 Socket 对象的 close() 方法关闭连接。