引子

下一篇标题是《深入理解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问题。

历史推荐

Redis集群搭建采坑总结

技术方案设计的方法

SpringBoot启动原理

学习Spring的思考框架

接下来一段时间会对大家进行网络通信的魔鬼训练-理解socket的更多相关文章

  1. 从配置读取一段时间(TimeSpan)

    C#的TimeSpan表示一段时间,DateTime表示一个时间点.TimeSpan可正可负,可与DateTime相加减,很方便,我喜欢. 代码中我们经常要表示一段时间,用一个统一的单位(时 或者 分 ...

  2. Win8.1开机黑屏一段时间才能登录

    最近发现开机后有一段时间黑屏过后才能进人登录界面,并且时间越来越长,网上查询了很多方法都没有效果,只能自己找了. 网上有一种方法提到用msconfig诊断判断或者安全启动来查看是否有黑屏,于是试了一下 ...

  3. 关于ScheduledExecutorService执行一段时间之后就不执行的问题

    在项目中使用java的定时任务时,有的时候执行一段时间后没任何反应了.这里有篇文章说了这个问题.猛击下面的链接. http://blog.163.com/scuqifuguang@126/blog/s ...

  4. 这段时间对c#和java的感受

    这段时间对c#和java的感受 虽然很多书上说语法相似,但实际这是一个接近于门外汉的看法 真正的不同是 c#对更贴近系统API,      而java倡导跨平台 因而c#语法关键字更多,更细, 而ja ...

  5. C#实现每隔一段时间执行代码(多线程)

    总结以下三种方法,实现c#每隔一段时间执行代码: 方法一:调用线程执行方法,在方法中实现死循环,每个循环Sleep设定时间: 方法二:使用System.Timers.Timer类: 方法三:使用Sys ...

  6. IIS服务器运行一段时间后卡死,且无法打开网站(IIS管理无响应,必须重启电脑)

    问题描述: 公司希望使用IIS配合网站显示一些订单跟进的情况并展示出来,所以我们在一台演示的Win7 Pro电脑上安装了IIS,但使用了一段时间后发现每过几天页面就无法正常访问了,而且打开IIS管理器 ...

  7. storm进程正常运行一段时间shut down,运维方式

    storm启动一段时间后,无征兆的停止了,然后nimbus,supervisor,ui所有的worker都stop了. 我用的storm是0.8.2版本的 nimbus中留下的log如下 -- :: ...

  8. 基于struts2、spring的应用闲置一段时间后报空指针错(转)

    在做struts2.spring网站时,在系统闲置一段时间后,访问页面会出错,第二次再访问就正常了.后来查了后台日志,发现是数据库连接关闭了,导致页面访问出错.页面上报空指针错误,错误没有保留,日志中 ...

  9. Activity后台运行一段时间回来crash问题的分析与解决

    最近做项目的时候碰到一个棘手的问题,花了不少时间才找到原因并解决.特此记录这个被我踩过的坑,希望其他朋友遇到此问题不要调到这坑里去了. 问题描述: 1.背景:我的app中某个界面的Activity是继 ...

随机推荐

  1. 通过大量实战案例分解Netty中是如何解决拆包黏包问题的?

    TCP传输协议是基于数据流传输的,而基于流化的数据是没有界限的,当客户端向服务端发送数据时,可能会把一个完整的数据报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大报文进行发送. 在这样的情况 ...

  2. 手把手教你学Dapr - 6. 发布订阅

    上一篇:手把手教你学Dapr - 5. 状态管理 介绍 发布/订阅模式允许微服务使用消息相互通信.生产者或发布者在不知道哪个应用程序将接收它们的情况下向主题发送消息.这涉及将它们写入输入通道.同样,消 ...

  3. CSS学习笔记:grid布局

    目录 一.Grid布局简介 二.Grid布局的一些概念 三. 容器元素属性 1. grid-template-* 1.1 网格行和列的设置 1.2 repeat的使用 1.3 使用fr 1.4 aut ...

  4. python实现膨胀与腐蚀

    目录: (一)膨胀 (二)腐蚀 (三)腐蚀代码(erode) (四)膨胀代码(dilate) (一)膨胀(或) (二)腐蚀(与) (三)腐蚀代码(erode) 1 def erode_demo(ima ...

  5. C语言通过指针数组和二维数组读取文件

    1 # include <stdio.h> 2 # include <stdlib.h> 3 # include <time.h> 4 # include < ...

  6. Spring Boot + MyBatis 多模块项目搭建教程

    一.前言 1.开发工具及系统环境 IDE:IntelliJ IDEA 2020.2.2 系统环境:Windows 2.项目目录结构 biz层:业务逻辑层 dao层:数据持久层 web层:请求处理层 二 ...

  7. Android连接远程数据库的避坑指南

    Android连接远程数据库的避坑指南 今天用Android Studio连接数据库时候,写了个测试连接的按钮,然后连接的时候报错了,报错信息: 2021-09-07 22:45:20.433 705 ...

  8. 『学了就忘』Linux权限管理 — 55、文件特殊权限

    目录 1.文件特殊权限说明 2.设置SetUID 3.检测SetUID的脚本 4.设置SetGID (1)针对文件的作用 (2)针对目录的作用 5.Sticky BIT 6.设定文件特殊权限 7.文件 ...

  9. WebGoat8.2.2-A8不安全的反序列化

    1.概念   使用反序列化在各编程语言中略有不同,如Java.PHP.Python.Ruby.C/C++,但在关键概念上是一样的     序列化:将(内存中的)对象转化成数据格式,以便存储或传输   ...

  10. SpringSecurity过滤器原理

    SpringSecurity原理 主要过滤器链 SpringSecurity的功能主要是由一系列的过滤器链相互配合完成的.验证一个过滤器之后放行到下一个过滤器链,然后到最后. 认证流程 过滤器作用 S ...