2

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

 2 years ago
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.
neoserver,ios ssh client

接下来一段时间会对大家进行网络通信的魔鬼训练-理解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++;
}
}

如果客户端没有启动,只启动服务端。上面提到会进入监听状态,这里程序用的是最简单的阻塞式监听。

 

1112728-20211210224524540-672995508.png

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

1112728-20211210224541518-1732146827.png

执行完后客户端和服务端都正常返回结果:

客户端:我知道我是任性太任性,伤透了你的心。我是追梦的人,追一生的缘分。    

服务端:我知道你是任性太任性,伤透了我的心。同是追梦的人,难舍难分。

客户端:我愿意嫁给你,你却不能答应我。       

服务端:你愿意嫁给你,我却不能向你承诺。

/**
* 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的思考框架


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK