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

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

引子


下一篇标题是《深入理解MQ生产端的底层通信过程》,建议文章读完之前、或者读完之后,再读一遍我之前写的《RabbitMQ设计原理解析》,结合理解一下。

 

我大学时流行过一个韩剧《大长今》,大女主长今是个女厨。她升级打怪的过程中,中国明朝来了个官员,是个吃货。那时候大明八方来朝,威风凛凛。那小朝鲜国可不敢怠慢,理论上应该大鱼大肉。人家长今凭借女主光环,给官员上了一桌素餐。官员勃然大怒,要把长今拉去砍头。长今解释说:官员脾胃失和,不适合大鱼大肉,让官员给她一段时间,天天吃她做的菜,他吃着吃着就会觉得素餐好吃了。官员就和她签了对赌协议。吃了一段时间素餐之后,官员向长今道歉,说明知道自己身体不适合大鱼大肉,但是管不住嘴,长今帮了他大忙。

 

其实要讲《深入理解MQ生产端的底层通信过程》这一篇之前我也做了很多的铺垫:从《架构师之路-https底层原理》的https协议,到《一个http请求进来都经过了什么(2021版)》实际上经过的物理通道,然后深入理解三次握手《懂得三境界-使用dubbo时请求超过问题》。有的文章读起来有点难度,我希望大家能像那位中国的官员一样,虽然不情愿但还是坚持一段时间,相信对于多数人来言对底层通信的理解会提升一个层次。

 

接下来是网络编程的干货时间,是下一篇文章的预备知识,不用担心,浅显易懂(多读几遍的话)。

 

socket编程究竟是什么?

 


socket的本质


socket的本质就是一种类型的文件,所以一个socket在进行读写操作时会对应一个文件描述符fd(file descriptor)。


1112728-20211210224347666-1890962901.png


socket的作用


1112728-20211210224403247-1993689842.png


上图是四层TCP/IP网络标准中,TCP/IP协议族的主要成员。今天只看上面两层。

 

最上层的应用层,涉及的协议封装的命令平时工作中也很常用,比如:ping、telnet。也有一些不是通过命令但也非常常用,比如:http。下一层的应用层有可靠的TCP协议和不可靠的UDP协议。平时工作中,常见的中间件如zookeeper、redis、dubbo这些都是使用TCP协议,因为这个内部封装完善,使用更简单。

 

要注意的是传输层操作是在内核空间完成的,就是说不是靠咱们平时的应用编码可以直接介入的。咱们平时直接用的就是应用层协议。想通过应用层操作传输层怎么办呢?这就用到了socket编程。

 

socket的简单原理


1112728-20211210224430616-1336013374.png


Socket位于TCP/IP之上,通过Socket可以方便的进行通信连接。对外屏蔽了复杂的TCP/IP。它是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件"(有对应的文件描述符fd),在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。


1112728-20211210224445196-473816581.png


要注意的是,想建立通信连接,需要一对socket。一个是客户端的socket,另外一个是服务端的socket。每个socket对应一个文件描述符fd。读和写都是通过这个fd完成的。但是一个socket对应两个缓冲区。一个读缓冲区,对应接收端;一个写缓冲区,对应发送端。

 

再次理解三次握手和四次挥手



1112728-20211210224458383-211303575.png


上面是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问题。

相关文章
|
2月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
在数字时代,网络应用成为连接世界的桥梁。Python凭借简洁的语法和丰富的库支持,成为开发高效网络应用的首选。本文通过实时聊天室案例,介绍Python Socket编程的基础与进阶技巧。基础篇涵盖服务器和客户端的建立与数据交换;进阶篇则探讨多线程与异步IO优化方案,助力提升应用性能。通过本案例,你将掌握Socket编程的核心技能,推动网络应用飞得更高、更远。
55 1
|
19天前
|
Kubernetes 网络协议 Python
Python网络编程:从Socket到Web应用
在信息时代,网络编程是软件开发的重要组成部分。Python作为多用途编程语言,提供了从Socket编程到Web应用开发的强大支持。本文将从基础的Socket编程入手,逐步深入到复杂的Web应用开发,涵盖Flask、Django等框架的应用,以及异步Web编程和微服务架构。通过本文,读者将全面了解Python在网络编程领域的应用。
18 1
|
22天前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
18 1
|
28天前
|
消息中间件 监控 网络协议
Python中的Socket魔法:如何利用socket模块构建强大的网络通信
本文介绍了Python的`socket`模块,讲解了其基本概念、语法和使用方法。通过简单的TCP服务器和客户端示例,展示了如何创建、绑定、监听、接受连接及发送/接收数据。进一步探讨了多用户聊天室的实现,并介绍了非阻塞IO和多路复用技术以提高并发处理能力。最后,讨论了`socket`模块在现代网络编程中的应用及其与其他通信方式的关系。
|
30天前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
2月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
在数字宇宙中,网络如同复杂脉络连接每个角落,Python Socket编程则是开启这一世界的钥匙。本文将引导你从基础概念入手,逐步掌握Socket编程,并通过实战示例构建TCP/UDP服务器与客户端。你将学会使用Python的socket模块进行网络通信,了解TCP与UDP的区别,并运用多线程与异步IO提升服务器性能。跟随本文指引,成为网络世界的建筑师,构建自己的网络帝国。
35 2
|
2月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
在网络编程的世界里,Socket编程是连接数据与服务的关键桥梁。对于初学者,这往往是最棘手的部分。本文将用Python带你轻松入门Socket编程,从创建TCP服务器与客户端的基础搭建,到处理并发连接的实战技巧,逐步揭开网络编程的神秘面纱。通过具体的代码示例,我们将掌握Socket的基本概念与操作,让你成为网络编程的高手。无论是简单的数据传输还是复杂的并发处理,Python都能助你一臂之力。希望这篇文章成为你网络编程旅程的良好开端。
55 3
|
2月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
在网络技术飞速发展的今天,掌握网络编程已成为开发者的重要技能。本文以Python为工具,带你从Socket编程基础逐步深入至进阶领域。首先介绍Socket的概念及TCP/UDP协议,接着演示如何用Python创建、绑定、监听Socket,实现数据收发;最后通过构建简单的聊天服务器,巩固所学知识。让初学者也能迅速上手,成为网络编程高手。
72 1
|
2月前
|
网络协议 安全 网络安全
震惊!Python Socket竟能如此玩转网络通信,基础到进阶全攻略!
【9月更文挑战第12天】在网络通信中,Socket编程是连接不同应用与服务的基石。本文通过问答形式,从基础到进阶全面解析Python Socket编程。涵盖Socket的重要性、创建TCP服务器与客户端、处理并发连接及进阶话题如非阻塞Socket、IO多路复用等,帮助读者深入了解并掌握网络通信的核心技术。
62 6
|
1月前
|
网络协议 测试技术 网络安全
Python编程-Socket网络编程
Python编程-Socket网络编程