第一篇只是开篇了数据结构和一些传输的例子。第二篇会讲网络编程传输协议在 Python 场景下如何使用。
根据上文的回复,顺序上会先讲 Tcp,下面进入正题。
Tcp 标准来自上个世纪 80 年代,也是历经了 30 多年的改进和优化。但是这些优化不是应用层的,避免长篇大论,有兴趣可以自己去了解下。
这里面一些观点后面也会用 Python 去验证。
那么学习网络协议,在第一篇里面是协议用的数据结构。下一步就是学习 Tcp 和常规的 http 镞有什么不同,先从结构上进行梳理,结构分别有:
Http 在应用层分为多个域,整体比较复杂,比 TCP 更复杂,只是 Http 使用场景比 Tcp 广域,所以熟能生巧和更多理解。
Http 镞和 Tcp 底层都是 socket,这个 http 在 web 那层做了大量限制和改造,所以有了更多域。
Http 镞不打算这里介绍,进入正题。Tcp 也是数据流,可以获得字节数组 bytes 长度的。
传输形态这层是由 socket 做的,Python 那层做了大量的包容,不用设置很复杂的 socket 设置。
socket 原生函数需要通过 nodename、servname、ai_flags、ai_family、ai_socktype、ai_protocol 来设置
ai_family:AF_INET Ipv4,目前大部分都是 Ipv4。 AF_INET6,就是 Ipv6。
ai_socktype:SOCK_STREAM 是数据流,也就是 TCP 的流设置。SOCK_DGRAM 是数据包,UDP 的包设置。
def tcp_client_options():
"""
tcp客户端设置的IPv4和数据流 (使用者)
:return:
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #通道设置
return sock
def tcp_server_options():
"""
tcp服务器设置(管理者)
:return:
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
"""
level=socket.SOL_SOCKET 所在哪个协议层
socket.SO_REUSEADDR 访问选项名 这个是一个参数int类型
"""
# SO_REUSEADDR端口释放后立即就可以被再次使用 1代表开启就可以使用
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
print(sock.getsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR))
服务器对比客户端来说是管理者身份,管理客户端链接,所以会多一个 setsockopt,设置 socket 规则或者叫配置。而客户端是使用者,所以只有设置通道(访问目标通道方式的)。
这个有个前置知识是 Tcp 握手和挥手,也是重要面试题,这里不做描述。
socket.SOL_SOCKET 是默认的代表你选择的协议层,也就是 socket 都是这个。
socket.SO_REUSEADDR 是设置,TCP 挥手后还能被使用,只能设置 1 和 0。
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
这里修改为 2 或者-1 下面打印还是 1,这个也是 Python 做了大量的封装包容的体现。
socket.SO_REUSEADDR 代表的是 optname 的控制方式,也就是挥手断开连接。
Http 是单次请求交互模式,也就是说你发一个数据流请求,会立即被投递到服务器,进行验证合法,合法后应答后
Tcp 不是,Tcp 是发一组(多段数据流请求后),但是服务器不是立即接受,是会拥堵在网络缓存区内,到达一定数量在发给服务器。
所以这个也是初学者经常会遇到的,假设服务器完全接收到客户端信息,客户端发了 10 次大小共 5000 个字节,服务器那边往往只会收到 3-4 次,大小总量也是 5000 个字节。这个可以在看完本文后,自己试试写个例子。
可以设置缓存区大小,缓存区大小会和 Tcp 重要概念有关,前面例子 plus:
def tcp_server_options():
"""
tcp服务器设置
:return:
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
print(sock.getsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR))
#设置发送缓存区容纳单位
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 16 * 1024)
print(f'Send -> {sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)}')
print(f'Revice -> {sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)}')
socket.SO_RCVBUF 是由协议层决定的,协议层这里常量是通过协议层默认大小来表示(双关),65535+1 的 65536,当然这个默认大小可以修改的,通常建议修改成 10 万,根据机器因地制宜。
socket.SO_SNDBUF 就是 16 * 1024 的,那么如果不设置会是多少呢。在 socket 层有很多双关,默认也是 65536。
还可以设置通信阻塞,这个阻塞是服务器处理客户端 socket 请求的,阻塞是会处理完才会接收同一个客户端的下一条请求,但是高并发场景下,多个 work 线程的 Tcp 服务器一般都是非阻塞的,这里的设置是全局设置的,会对整个服务器当前启动生效。
sock.setblocking(False) #设置非阻塞的。
还可以设置超时时间,但是不要画鱼加红烧肉这种做法,添加了超时时间会和上面设置阻塞有一些冲突,会影响正常设置了缓存区大小等,所以不推荐使用。
这个和平时测试工具使用的 requests 里面 timeout 不一样。
常规设置都已经讲了,下面讲服务器如何去让客户端投递数据给他。
2.链接地址
根本上没差别,网络传输从 1 端到另外 1 端,要传输都需要有地址 接收者 iP+ 端口。
客户端需要知道服务器的链接地址才能把消息投递到正确的地方,服务器拥有接收者 iP 会分配一个地址也就是端口,然后 bind 端口,监听链接到这个地址去管理客户端。
服务器还会有一个最大支持链接数,客户端是没有这个设置的,下面例子里面 max=5000,所以客户端和服务器关系是多对一的,最多支持 5000 个客户端链接到同个服务器。
看到上面,划重点关键字 bind 和 listen,给个例子,区分客户端和服务器代码:
def server_listener(addr: tuple, max: int):
"""
服务器监听 最大支持5000个链接数
:param max:最大支持链接数
:param addr:元组
:return:
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置挥手后可以使用
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(addr)
# 最大支持max个链接数
sock.listen(max)
return sock
if __name__ == '__main__':
server_listener(("0.0.0.0", 12580),5000)
http 的服务器因为包装的很好,并不会有这种设置,服务器都是一个监听客户端 fd 和轮询管理这些 fd 的。
客户端&服务器用同一套 socket options,这样服务器才能接收链接客户端的消息,fd=sock.fileno()。
获取方式以及客户端链接代码,如下例子:
def create_client():
"""
创建客户端
:return:
"""
# 客户端和服务器设置通道的socket肯定是要一样的,否则链接不过来
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockfd = sock.fileno()
print(f"客户端fd={sockfd}")
sock.connect(("127.0.0.1", 12580))
return sock
if __name__ == '__main__':
client = create_client()
print(client)
客户端链接代码里面,地址不能是 0.0.0.1,本机测试也只能是 127.0.0.1。
connect_ex() 不为 0,会返回 error 的码,会在和协议号和错误协议号串联一起的后面章节来讲。
服务器监听客户端 fd 的代码如下,核心函数是 accept():
def accept_client_connect(listener_sock):
"""
服务器处理 确认客户端链接 核心方法是accept()
:param listener_sock:服务器sock对象
:return:
"""
new_conn = listener_sock.accept()
if new_conn == None:
return
sock, addr = new_conn
# 服务器监听可以拿到那边拿sock的fd
print(f"---accept_client_connection, sockfd={sock.fileno()}, addr={addr}")
return sock
if __name__ == '__main__':
sock = server_listener(("0.0.0.0", 12580), 5000)
sock_ = accept_client_connect(sock)
print(sock_)
最终打印
---accept_client_connection, sockfd=512, addr=('127.0.0.1', 14024)
<socket.socket fd=512, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 12580), raddr=('127.0.0.1', 14024)>
addr 端口就是服务器给客户端临时分配的端口,服务器就是通过字典管理,sockfd 和 addr[1],下面这段描述可以让其他语言也可以理解。
ClientInfo = {"client_fd":sockfd,"fd_port":addr[1]}
ClientGroup = []ClientInfo
{"gateway_threads":ClientGroup}
Workthread 分属不同作用,服务器里面的 gateway 服务 bind clientGroup 数组,这些细节后面章节会有具体的例子。
预告 socket 发包部分流程
step1: socket options(第二篇)
step2: 断开 socket,struct+pack 包 (数据结构)+ 压包 (数据结构在第一篇,第三篇是断开 socket 和 struct)
step3: 压缩的多种形式 (第四篇)
step4: 加密的多种形式 (第五篇)
总结