网络编程之socket模块

网络编程之socket模块

在上一篇博文中,我们介绍了osi的七层协议,从物理层简单介绍到了传输层,说到了传输层。

每一层都有相对要遵循的协议如图:

img

其中,物理层就是二进制,应用层可能遵循的协议太多了,有http,https等。

传输层之TCP与UDP协议

TCP与UDP都是用来规定通信方式的。

TCP协议

又称可靠协议(数据不容易丢失)

三次握手

image

客户端发送连接请求,服务端接收到了并反馈接收可以建立从客户端到服务端的通道,与此同时,服务端可以发送连接请求,让客户端同意服务端向客户端建立通道,这样就建立了双向通道。

虽然但是,造成数据不容易丢失的原因不是因为有双向通道,而是因为有反馈机制。

给对方发送数据的时候,会保留一个副本,直到对方回应收到了消息,才会删除本地数据,否则会在一定时间内反复的发送。

  • 三次握手后成功建立连接,连接双方就能通过双向通道发送数据了

  • 洪水攻击

    同一时间有大量的客户端请求建立链接,会导致服务端一致处于SYN_RCVD状态

  • 服务端如何区分客户端建立链接的请求

    可以对请求做唯一标识,如它的IP地址等

四次挥手

image

  1. 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。

  2. 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

    注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。

  4. 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

UDP协议

也称之为数据报协议和不可靠协议,没有什么反馈机制,一端发送了数据后就不管了,另外一端能不能收到数据不会影响发送端只发一次。

qq最初就是使用最基本的udp协议来传输消息。

而现在还是用udp,只是添加了很多功能来保障数据的安全。

socket模块

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

img

简而言之,socket帮我们把传输层及以下的处理全做了,调这个模块就能帮助我们通过网络完成两个程序间的通讯。

TCP协议的socket代码实现

# 服务端
import socket

# 1.产生一个socket对象并指定采用的通信版本和协议(TCP)
server = socket.socket()  # 括号内不写参数 默认就是TCP协议  family=AF_INET基于网络的套接字 type=SOCK_STREAM流式协议即TCP
# 2.绑定一个固定的地址(服务端必备的条件)
server.bind(('127.0.0.1', 8080))  # 127.0.0.1为本地回环地址 只有自己的电脑可以访问
# 3.设立半连接池(暂且忽略)
server.listen(5)
# 4.等待接客
sock, addr = server.accept()  # return sock, addr  三次握手
print(sock, addr)  # sock就是双向通道 addr就是客户端地址
# 5.服务客人
data = sock.recv(1024)  # 接收客户端发送过来的消息 1024字节
print(data.decode('utf8'))
sock.send('我是服务端~诶嘿'.encode('utf8'))  # 给客户端发送消息 注意消息必须是bytes类型
# 6.关闭双向通道
sock.close()  # 四次挥手
# 7.关闭服务端
server.close()  # 关闭服务端(一般不用)



# 客户端
import socket

# 1.生成socket对象指定类型和协议
client = socket.socket()
# 2.通过服务端的地址链接服务端
client.connect(('127.0.0.1', 8080))
# 3.直接给服务端发送消息
client.send('我是客户端~哦吼'.encode('utf8'))
# 4.接收服务端发送过来的消息
data = client.recv(1024)
print(data.decode('utf8'))
# 5.断开与服务端的链接
client.close()

socket模块方法总结

  • socket() :产生一个socket对象并指定采用的通信版本和协议

  • bind((ip,port)):绑定一个固定的ip和端口号,服务端必备

  • listen(5) :设立半连接池,容量为5

  • connect((ip,port)):向服务端发送链接请求,并接收握手回馈信号(三次握手)

  • accept():等待客户端的链接请求,并发出握手回馈信号(三次握手)
    返回一个双向通道和客户端地址(ip和port)
    服务端要通过双向通道来send和recv,因为只有这个通道有客户端的地址信息。

  • send(二进制数据):可以让建立协议的两端互相发送消息

  • recv(字节数):可以让建立协议的两端互相接收消息

网络通讯简要优化

1.聊天内容自定义
	针对消息采用input获取
2.让聊天循环起来
	将聊天的部分用循环包起来
3.用户输入的消息不能为空
	本质其实是两边不能都是recv或者send,一定是一方收一方发 
    在win中如果发空,会报错
4.服务端多次重启可能会报错
	Address already in use 主要是mac电脑会报
  	我们可以通过改端口号的方式(服务端的bind和客户端connect都要改)来简单解决端口占用的问题
5.当客户端异常断开的情况下 如何让服务端继续服务其他客人
	windows服务端会直接报错,mac服务端会有一段时间反复接收空消息延迟报错	
  	处理方式:异常处理、空消息判断

半连接池

server.listen(5)  # 半连接池

在服务端的程序中,我们的server对象可以通过这一句代码来监听有哪些程序尝试连接我们的服务端程序,当服务端正和某个客户端建立好通道进行联系时,其他的客户端就得排队了,当服务端断开与之前客户端的联系,新的客户端就可以与服务端进行连接了(前提是服务端还有accept进行回应)

如:

# 服务端
import socket

server = socket.socket()
server.bind(('127.0.0.1', 8888))
server.listen(5)

while True:
    # 每次循环开始都可以接收一次连接
    sock, addr = server.accept()
    print(f'{addr}已接入')
    # 模拟对话开始
    bytes_msg = sock.recv(1024)
    print(bytes_msg.decode('utf8'))
    sock.send('服务端向你扔了一只小狗'.encode('utf8'))
    # 模拟对话结束
    
# 客户端
import socket

client = socket.socket()
client.connect(('127.0.0.1', 8888))
anyone = input('扔一只什么给服务端吧:')
client.send(f'扔了只{anyone}'.encode('utf8'))
print(client.recv(1024).decode('utf8'))

在上述程序中,我们通过客户端不断的向服务端发起请求,交流会话。

image

将这个打开,就可以让客户端程序同时开启多个,当我们发起6个以上的客户端连接请求,第一个正在于服务端对话,而后面五个在半连接池中等待,再由客户端在此时想要向我们发送这个端口发送请求就会连接异常。

所以这个listen中的数字就表示可以容纳的等待连接数量。

image

TCP协议的粘包现象

粘包现象

当我们从客户端连续发送send内容,从执行recv来接收时,会发现,应该分三次发送到服务端的内容,全部被第一次recv接收,并且是粘结在了一起。这种现象称为粘包现象。

image

粘包现象的原因

  • 流式协议

    比喻来说:数据像水流一样传输,没有间隔

    实际上,TCP协议会针对数据量较小且发送间隔较短的多条数据一次性合并打包进行发送

  • 每次发送数据不得而知

    我们每次发送多大的数据,发送端自己知道,但是接收端不知道,而recv的执行恰恰要指定接收最大接收字节数的参数。在面对数据流时,接收端无法得知在哪里停止。

粘包解决方案

针对粘包的原因,我们可以得到以下解决方案,总体思路就是:

我们让会话双方约定,文件在传输前,先确定文件的大小,这样接收方就知道在何时停止接收。

但是文件大小怎么让接收方知道呢,还是要通过网络传输的,如果基于TCP协议,我们还是需要面对类似的问题,文件大小这一数据的长度又不确定了。

为此就需要借助一个模块了:

struct模块:报头

文件大小这一个数据可以通过struct模块来固定其长度,将这个固定长度的数据发给接收方,接收方是可以提前准备好的,如:

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。

发送时 接收时
先发送struct转换好的数据长度4字节 先接受4个字节使用struct转换成数字来获取要接收的数据长度
再发送数据 再按照长度接收数据
# 客户端
'''
conn.send(struct.pack('i',len(back_msg)))  # 先发back_msg的长度
conn.sendall(back_msg)  #  在发真实的内容
'''

import os
import socket
import struct

client = socket.socket()
client.connect(('127.0.0.1', 8888))

file_size = os.path.getsize('学习资料')
client.send(struct.pack('i', file_size))
with open('学习资料', 'r', encoding='utf8') as f:
    for line in f:
        client.send(line.encode('utf8'))
# 服务端
'''
l=sever.recv(4)  # 得到长度报头
x=struct.unpack('i',l)[0]  # 通过struct模块解析长度是多少
sever.recv(x[0])  # 按照这个长度去收内容
'''
import socket
import struct

server = socket.socket()
server.bind(('127.0.0.1', 8888))
server.listen(5)
sock, addr = server.accept()

length = struct.unpack('i', sock.recv(4))  # 得到文件长度
print(length)  # (375,)  解析出来是元组形式
with open('现在是我的学习资料', 'wb') as f:
    f.write(sock.recv(length[0]))  # 转存到本地

这样就基本解决了粘包问题

报头的报头

在上述解决方案中,我们没有提到,struct模块能压缩的数字是有限的,具体如下图:

img

我们之前是用i模式来压缩的,不过无论用什么模式,这个数字都有上限。

也就意味着,当文件过大时,文件大小的数字量超出了sturct模块的可压缩范围,我们就无法压缩这个数字了。

不卖关子了,现在所有的最成熟的解决方案是报头的报头,就是指在文件传输前将文件包括长度在内的信息先传输过去,在文件信息传输之前先将文件信息的数据长度传输过去。

如我们在浏览百度云端的文件时,虽然查看文件还要下载,但是在此之前就已经可以看到文件的详细信息了。

image

代码实现
# 服务端
import json
import socket
import struct

server = socket.socket()
server.bind(('127.0.0.1', 8888))
server.listen(5)
sock, addr = server.accept()

length = struct.unpack('i', sock.recv(4))  # 得到文件信息长度
file_infos = json.loads(sock.recv(length[0]))  # 得到文件信息二进制并反序列化为数据
total_size = file_infos.get('file_size')  # 拿到数据大小
with open('现在是我的学习资料', 'wb') as f:
    recv_size = 0  # 初始接收了0字节的数据
    while recv_size < total_size:  # 当接收量不在小于文件总大小,则停止接收
        data = sock.recv(1024)  # 每次接收1024个字节
        f.write(data)  # 写入本地
        recv_size += len(data)  # 每次接收后,将接收到的数据量记录下来
# 客户端
import json
import os
import socket
import struct

client = socket.socket()
client.connect(('127.0.0.1', 8888))

file_size = os.path.getsize('学习资料')
file_infos = {
    'file_name': '电脑配件',
    'file_size': file_size,
}
bytes_infos = json.dumps(file_infos).encode('utf8')  # 数据先变成json再编码为二进制以便还原
client.send(struct.pack('i', len(bytes_infos)))  # 先发送文件信息的长度
client.send(bytes_infos)  # 再发送文件信息
with open('学习资料', 'rb') as f:
    for line in f:  # 流式协议,所以分几次发都是粘着的(但是这样可以减少发送端的内存压力)
        client.send(line)  # 最终发送文件本身

UDP协议的socket代码实现

UDP服务端
# 服务端
import socket

server = socket.socket(type=socket.SOCK_DGRAM)  # 建立了UDP协议的连接
server.bind(('127.0.0.1', 8080))  # 依然可以绑定某个端口号
while True:
    data, addr = server.recvfrom(1024)  # recvfrom接收udp协议的消息,返回信息和发送端的地址
    print(f'来自{addr}的消息:{data.decode("utf8")}')
    server.sendto('收到了'.encode('utf8'), addr)  # sendto(信息,接收端地址)
UDP客户端
# 客户端
import socket

conn = socket.socket(type=socket.SOCK_DGRAM)  # 建立了UDP协议的连接

conn.sendto('我发送了个消息,你收到了吗'.encode(), ('127.0.0.1', 8080))
data, addr = conn.recvfrom(1024)
print(f'来自{addr}的回复:{data.decode("utf8")}')
posted @ 2022-11-16 18:35  leethon  阅读(39)  评论(0编辑  收藏  举报