服务器通信模型(一): socket编程中accept函数的深层探究

服务器通信模型(一): socket编程中accept函数的深层探究

本系列文章想记录一下对于IO多路复用通信模型的学习与总结,同时对于redis的网络通信模型和java的nio Selector编程框架进行一个横向比较,探讨了它的应用场景。之后如果更深入学习了netty、memcache这些框架也会持续跟进本系列文章。

经典回顾:socket编程

在讨论复杂的epoll编程模型之前,先回顾一下计网中的socket编程相关的知识,同时做一些关于socket本质的深挖。在socket编程当中,TCP socket客户端的流程如下图所示(为了方便找了一张网图):


同时为了进一步讨论的方便,我选用python作为测试语言。这里为什么用python为什么不用java呢?因为python的socket函数接口与C语言的高度一致,而java有很多封装,流程和函数调用不够明晰。(这又反映了java的面向对象编程哲学:屏蔽底层,方便开发者)

贴出python的server端与client端的调试代码:

server:

from socket import *

# 创建tcp流式套接字
serverSocket = socket(AF_INET, SOCK_STREAM)
# 绑定端口
serverSocket.bind(('', 8888))
print(serverSocket)

# 监听网络端口
serverSocket.listen()
# 接收客户端连接
accept, addr = serverSocket.accept()
print(accept)

# 接收消息
message = accept.recvfrom(1024)
print(f"message: {message}")

client:

from socket import *

# 创建流式套接字
s = socket(AF_INET, SOCK_STREAM)
addr = ("127.0.0.1", 8888)
# 连接server
s.connect(addr)
print(s)
# 发送消息
s.sendto("Hello Server".encode(), addr)
input()

accept函数的奥秘

在上图中注意到,server创建了一个套接字s (原始socket),在accept之后会生成新的套接字ns (即new socket),之后调用recv和send会在ns套接字上进行消息的收发,那么有以下几个问题:

  1. 套接字s和ns有什么区别?
  2. 套接字s能否进行消息的收发或连接别的server?
  3. 套接字ns能否通过bind, listen函数创建新的socket?
  4. 套接字ns收发消息走的是原来的套接字s吗?为什么没有新的端口映射到ns?

下面针对以上问题,以下通过python代码和windows cmd下的netstat -ant命令来进行演示:


  1. 套接字s和ns有什么区别?

分别运行以上server和client的代码,先在python下打印输出socket的状态

server:

client:

可以看到server端s和ns对应的socket具有不同的文件描述符,这符合socket为文件的设计思想

在windows cmd下的netstat -ant下观察socket的状态:


从图中可以看到第一个TCP连接(server端套接字s)的状态是LISTENING,而第二个对应的是server端套接字ns,状态是ESTABLISHED,第三个是client端套接字。

可以看到,同时accept建立的socket具有与原来的socket完全不一样的状态,因此也就有了不一样的职能,原来的套接字通过Python的bind和listen函数变成了具有LISTENING状态的套接字,负责连接的监控与建立,然后新建一个新的socket,负责后续的消息收发

在“外部地址”这一列,可以看到第一个TCP连接的ip:port是全0,在计算机网络中,这意味着整个互联网,即可以接受来自互联网的所有请求,而第二个TCP连接的ip:port是client的ip:port,代表着一个特定的TCP连接。

2. 套接字s能否进行消息的收发或连接别的server?

答案是都不行。

可以用python分别进行验证,server端的套接字s调用recv函数

recv()报错信息如下

OSError: [WinError 10057] 由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接收数据的请求没有被接受。

分析:在对套接字s进行打印的时候,是没有远端地址raddr这一项的,因此OS会认为该socket并没有与别的socket进行连接,因此报错。

connect报错信息如下

这里connect的参数是一个真实存在的ip:port,但是无法连接,原因依旧与当前socket的状态有关。

3. 套接字ns能否通过bind, listen函数创建新的socket?

答案依旧是都不行。这里就不试验了,感兴趣的话读者可以自行测试,原因依旧是跟socket状态相关。

4. 套接字ns收发消息走的是原来的套接字s吗?为什么没有新的端口映射到ns?

这是一个很关键的问题,首先可以肯定地回答,套接字收发消息走的是新建立的tcp套接字ns,这也是socket作为通信最小单位的意义。同时,accept创建的socket并没有进行端口的占有,而是复制了原来socet fd的本地IP和端口号,并且记录了连接过来的客户端的IP和端口号。同时也可以设想,如果每个socket都要映射一个端口号,首先端口的数量65535完全不够,而且由于服务器防火墙的存在,端口是不能随意申请的,否则需要运维人员一个个地配置socket映射。

小结

该系列的第一篇首先探究了socket的相关更深层的知识,方便以后进一步讨论。说“深层”的客观原因是之前确实仅仅在计算机网络中这门课中用到过socket,手写过简单的http代理服务器,但并没有深入系统地学习Linux大部头《Unix环境高级编程》《Unix网络编程》,希望以后有时间把这部分知识补上来。可能到时候会有完全不一样的体会了哈哈哈。

参考文章

socket的accept函数解析以及服务器和多个客户端的端口问题_L未若的博客-CSDN博客

这篇文章有作者自己的思考和实验探究,感觉很不错。

系列文章

Hades:服务器通信模型(一): socket编程中accept函数的深层探究
Hades:服务器通信模型(二): Reactor与Proactor 模式
Hades:服务器通信模型(三): Linux的IO多路复用模型——Select、Poll、Epoll
Hades:服务器通信模型(四): Netty线程模型及其模拟实现
Hades:服务器通信模型(五): Redis的网络通信模型
编辑于 2022-04-05 22:04