

接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket
source link: https://www.cnblogs.com/xiexj/p/15674135.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket
引子
下一篇标题是《深入理解MQ生产端的底层通信过程》,建议文章读完之前、或者读完之后,再读一遍我之前写的《RabbitMQ设计原理解析》,结合理解一下。
我大学时流行过一个韩剧《大长今》,大女主长今是个女厨。她升级打怪的过程中,中国明朝来了个官员,是个吃货。那时候大明八方来朝,威风凛凛。那小朝鲜国可不敢怠慢,理论上应该大鱼大肉。人家长今凭借女主光环,给官员上了一桌素餐。官员勃然大怒,要把长今拉去砍头。长今解释说:官员脾胃失和,不适合大鱼大肉,让官员给她一段时间,天天吃她做的菜,他吃着吃着就会觉得素餐好吃了。官员就和她签了对赌协议。吃了一段时间素餐之后,官员向长今道歉,说明知道自己身体不适合大鱼大肉,但是管不住嘴,长今帮了他大忙。
其实要讲《深入理解MQ生产端的底层通信过程》这一篇之前我也做了很多的铺垫:从《架构师之路-https底层原理》的https协议,到《一个http请求进来都经过了什么(2021版)》实际上经过的物理通道,然后深入理解三次握手《懂得三境界-使用dubbo时请求超过问题》。有的文章读起来有点难度,我希望大家能像那位中国的官员一样,虽然不情愿但还是坚持一段时间,相信对于多数人来言对底层通信的理解会提升一个层次。
接下来是网络编程的干货时间,是下一篇文章的预备知识,不用担心,浅显易懂(多读几遍的话)。
socket编程究竟是什么?
socket的本质
socket的本质就是一种类型的文件,所以一个socket在进行读写操作时会对应一个文件描述符fd(file descriptor)。
socket的作用
上图是四层TCP/IP网络标准中,TCP/IP协议族的主要成员。今天只看上面两层。
最上层的应用层,涉及的协议封装的命令平时工作中也很常用,比如:ping、telnet。也有一些不是通过命令但也非常常用,比如:http。下一层的应用层有可靠的TCP协议和不可靠的UDP协议。平时工作中,常见的中间件如zookeeper、redis、dubbo这些都是使用TCP协议,因为这个内部封装完善,使用更简单。
要注意的是传输层操作是在内核空间完成的,就是说不是靠咱们平时的应用编码可以直接介入的。咱们平时直接用的就是应用层协议。想通过应用层操作传输层怎么办呢?这就用到了socket编程。
socket的简单原理
Socket位于TCP/IP之上,通过Socket可以方便的进行通信连接。对外屏蔽了复杂的TCP/IP。它是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件"(有对应的文件描述符fd),在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
要注意的是,想建立通信连接,需要一对socket。一个是客户端的socket,另外一个是服务端的socket。每个socket对应一个文件描述符fd。读和写都是通过这个fd完成的。但是一个socket对应两个缓冲区。一个读缓冲区,对应接收端;一个写缓冲区,对应发送端。
再次理解三次握手和四次挥手
上面是TCP下通信调用Linux Socket API流程。
服务端一启动,就要先调用socket函数建立socket,socket会调用bind函数绑定对应的IP和端口。之后listen函数的作用可能和大多数人理解都不同,它的主要作用是设置监听上限。就是允许多少个客户端进行连接。accept函数是以监听客户端请求的。调用了这个函数就相当于咱们平时的thrift服务端启动了。具备了三次握手的条件。
这时候客户端也建立一个套接字,调用connect函数执行三次握手。成功后,服务端调用accept函数新建立一个socket专门用来和这个客户端进行通信。之前的老socket用来监听别的请求。这里注意:客户端套接字和服务端套接字是成对出现。但是这里一共出现了三个套接字。因为客户端和服务端正式握手时,服务端使用的是新建的socket来处理这个客户端的通信。因为老的socket还需要监听是否有其他的客户端。
接下来的send、recv和write函数都是处理数据的,这里不过多解释。
客户端使用close函数进行四次挥手关闭与服务端的连接。服务端使用recv函数接收到了关闭请求执行挥手。
程序理解
Linux Socket API很多语言都有对它的实现,差不多的。这里因为我本人更熟悉Java,这里用Java做说明。
这里使用我自己之前写的《懂了!国际算法体系对称算法DES原理》中的代码,去掉加解密的部分:
public void client() throws Exception {
int i = 1;
while (i <= 2) {
Socket socket = new Socket("127.0.0.1", 520);
//向服务器端第一次发送字符串
OutputStream netOut = socket.getOutputStream();
InputStream io = socket.getInputStream();
String msg = i == 1 ? "客户端:我知道我是任性太任性,伤透了你的心。我是追梦的人,追一生的缘分。" :
"客户端:我愿意嫁给你,你却不能答应我。";
System.out.println(msg);
netOut.write(msg.getBytes());
netOut.flush();
byte[] bytes = new byte[i == 1 ? 104 : 64];
io.read(bytes);
String response = new String(bytes);
System.out.println(response);
netOut.close();
io.close();
socket.close();
i++;
}
}
如果不开服务端,只执行客户端代码,则报异常:
java.net.ConnectException: Connection refused: connect
咱们来看这个代码做了什么:启动客户端,与服务端建立连接,理论上要调用linux的socket和connect两个函数。这个动作在new Socket实例化的时候是做了的:
private Socket(SocketAddress address, SocketAddress localAddr,
boolean stream) throws IOException {
setImpl();
// backward compatibility
if (address == null)
throw new NullPointerException();
try {
createImpl(stream);
if (localAddr != null)
bind(localAddr);
connect(address);
} catch (IOException | IllegalArgumentException | SecurityException e) {
try {
close();
} catch (IOException ce) {
e.addSuppressed(ce);
}
throw e;
}
}
然后咱们看服务端代码:
@Test
public void server() throws Exception {
ServerSocket serverSocket = new ServerSocket(520);
int i = 1;
while (i <= 2) {
String msg = i == 1 ? "服务端:我知道你是任性太任性,伤透了我的心。同是追梦的人,难舍难分。" :
"服务端:你愿意嫁给你,我却不能向你承诺。";
Socket socket = serverSocket.accept();
InputStream io = socket.getInputStream();
byte[] bytes = new byte[i == 1 ? 112 : 64];
io.read(bytes);
System.out.println(new String(bytes));
OutputStream os = socket.getOutputStream();
System.out.println(msg);
byte[] outBytes = msg.getBytes();
os.write(outBytes);
os.flush();
os.close();
io.close();
i++;
}
}
如果客户端没有启动,只启动服务端。上面提到会进入监听状态,这里程序用的是最简单的阻塞式监听。

如上所示,在执行accept方法时,server开始打圈圈,阻塞了。客户端启动后,server进行到了下面读取数据的阶段:

执行完后客户端和服务端都正常返回结果:
客户端:我知道我是任性太任性,伤透了你的心。我是追梦的人,追一生的缘分。
服务端:我知道你是任性太任性,伤透了我的心。同是追梦的人,难舍难分。
客户端:我愿意嫁给你,你却不能答应我。
服务端:你愿意嫁给你,我却不能向你承诺。
/**
* Create a server with the specified port, listen backlog, and
* local IP address to bind to. The <i>bindAddr</i> argument
* can be used on a multi-homed host for a ServerSocket that
* will only accept connect requests to one of its addresses.
* If <i>bindAddr</i> is null, it will default accepting
* connections on any/all local addresses.
* The port must be between 0 and 65535, inclusive.
* A port number of {@code 0} means that the port number is
* automatically allocated, typically from an ephemeral port range.
* This port number can then be retrieved by calling
* {@link #getLocalPort getLocalPort}.
*
* <P>If there is a security manager, this method
* calls its {@code checkListen} method
* with the {@code port} argument
* as its argument to ensure the operation is allowed.
* This could result in a SecurityException.
*
* The {@code backlog} argument is the requested maximum number of
* pending connections on the socket. Its exact semantics are implementation
* specific. In particular, an implementation may impose a maximum length
* or may choose to ignore the parameter altogther. The value provided
* should be greater than {@code 0}. If it is less than or equal to
* {@code 0}, then an implementation specific default will be used.
* <P>
* @param port the port number, or {@code 0} to use a port
* number that is automatically allocated.
* @param backlog requested maximum length of the queue of incoming
* connections.
* @param bindAddr the local InetAddress the server will bind to
*
* @throws SecurityException if a security manager exists and
* its {@code checkListen} method doesn't allow the operation.
*
* @throws IOException if an I/O error occurs when opening the socket.
* @exception IllegalArgumentException if the port parameter is outside
* the specified range of valid port values, which is between
* 0 and 65535, inclusive.
*
* @see SocketOptions
* @see SocketImpl
* @see SecurityManager#checkListen
* @since JDK1.1
*/
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
这是服务端ServerSocket的实例化过程,注意一下backlog这个参数,就是《懂得三境界-使用dubbo时请求超过问题》里产生问题的罪魁祸首。
这里注释已经说的很明白了,我就直接翻译成中文:
创建一个指定端口的服务端,监听backlog和绑定的本地IP。bindAddr参数可以用于多个网络端口的主机。但是一个服务端Socket只能连接到其中一个地址。如果bindAddr参数为空,它会默认连接本机。端口值必须介于0到65535之间。端口号通常是从临时端口段(1024之后)动态指定的,可以通过getLocalPort方法把值取出来。
如果有安全管理(在上面代码里看不到安全管理是因为这段代码在bind方法里面),则会对端口进行权限检查,确保操作是允许的。这一步可能引发安全检查异常。
backlog参数是这个socket等待连接的最大允许请求量。它的精确语义和实现有关。需要重点来说的是,这个实现可以选择自己指定一个上限同时选择忽略这个参数,并且这个自己指定的上线还要比这里的backlog参数值大。如果实现里是小于等于这里的backlog参数的,就会直接使用实现的默认值。
总结
强烈建议读完本文再次读一遍《懂得三境界-使用dubbo时请求超过问题》,深入理解backlog问题。
历史推荐
Recommend
-
35
转于自己在公司的Blog: http://pt.alibaba-inc.com/wp/experience_1301/code-detail.html 最近一直担心Dubbo分布式服务框架后续如果维护人员增多或变更,会出现质量的下 ...
-
58
写作意图 这篇文章用于总结一些javascript语言中常见的易混淆点。 call | apply | bind 在js中,最诡异莫测的莫过于this了,理解的不够深入或是应用场景略微复杂,使用时就会出现各种意想不到的错误。所以,在很多时候,我们需要手动指定上
-
131
Base64 是我们常用的编码方式,广泛用于邮件编码、数据签名/数据校验编码以及HTML/XML复杂数据编码。这本来是一个花两分钟了解一下就无需关注的技术,但是最近线上遇到一个相关问题。于是重新梳理了一下 Base64, 发现了一些以前未曾注意到...
-
79
随着算法的诞生,智人似乎终于制造出了一种可以实现一切愿望的工具。 如今,算法已经无孔不入,我们的工作、社交、医疗、工业、运输、贸易无不有算法的重大参与。各种算法正改变...
-
3
为了加快信息处理脑细胞会对 DNA 进行拆分重组 wanwan (42055)发表于 2021年08月31日 13时...
-
8
摘要: 网络是通信互联的基础,Node.js提供了net、http、dgram等模块,分别用来实现TCP、HTTP、UDP的通信,本文主要对使用Node.js的TCP通信部份进行实践记录。 本文分享自华为云社区《
-
7
韩国金融服务委员会官员确认不会对NFT进行监管 • 11 小时前 碳链价值APP讯,韩...
-
9
[流言板]湖人截止日前没有进行操作,他们接下来将专注于买断市场虎扑篮球资讯(1233级)楼主2022-02-10 20:06:31
-
6
2023-01-29 15:29 土耳其成立加密协会对加密产业进行监督 据 Bitcoin.com 1 月 29 日报道,土耳其成立了一个新组织,名为“加密行业发展、监测和报告协会”,旨在监督和帮助发展该国的加密行业。它的首要任务将是解决...
-
3
V2EX › 问与答 最近一段时间大家如果集中注意力很有可能会发现 : 时空穿越者
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK