TCP\UDP协议 socket模块

2023-02-13,,,,

目录
传输层主要协议
TCP协议
三次握手
TCP协议反馈机制
四次挥手
洪水攻击
UDP协议
socket模块
socket代码简介
socket.socket()
server.bind()
server.accept()
sock.recv() sock.send()
sock.close() server.close()
client.connect()
问题和优化
用户输入的消息不能为空
服务端多次重启报错
客户端异常断开
半连接池
server.listen
示例
练习
UDP单向通信
模拟QQ聊天
总结
又回来总结

传输层主要协议

传输层有很多协议,比如TCP、UDP协议。
TCP与UDP都是用来规定通信方式的
通信的时候可以随心所欲的聊 也可以遵循一些协议符合要求的聊
随性所欲的聊:文字 图片 视频 小油腻话 你侬我侬
遵循一些协议:开头带尊称 首行空两格 只准用官话 不能打情骂俏
ps:不遵循上述协议也可以通信 只不过遵循了更合规合法合理!!!

TCP协议

TCP协议也称为流式协议、可靠协议(数据不容易丢失)

三次握手

    经过这两步:C朝S建立了通道,这个通道是单向的,C可以给S发数据!

    如果S想给C发数据怎么办?又要走两步!才能建立S到C的通道。

    注意:C给S发信息就走通道1,S给C发信息走通道2,这两个通道互不干扰!两个通道都是单向的!
    这不是四次吗?为什么说是三次握手?

    因为中间两次可以整合为一次,也就是两条信息一起发了!总得来说只需要通信三次!这就是三次握手 建链接!

    附上图片:

TCP协议反馈机制

TCP协议之所以比UDP协议可靠 是建立了双向通道 对吗?

TCP协议之所以比UDP协议可靠 原因在与TCP协议发送消息有反馈机制!
(你必须要回复说,你收到了。如果你不回复,我就再次给你发同一份数据!)
基于TCP发送的消息会在本地先保存该消息 如果地方确认收到才会删除 否则在一定的时间内会频繁的多次发送直到确认或者超时为止。
我发给你一份 内存里也临时存一份 当你不回复,我隔个5秒10秒再给你发,如果我坚持给你发了30分钟,你都不回复,我就默认你嗝屁了, 就把临时保存的数据删掉。
另外当你回复:收到了! 我也会把内存里临时存的数据删掉。
# 也就是需要一个确认 确认通过了才会继续发 这就是反馈机制。

四次挥手

由任意一方发起断开的请求:如图C发送断开的请求,C同意。所以将会断开C到S的通道。

再经历两步,断开S到C的通道:

中间的两步为什么不能合并?

因为C已经没有数据给S了 ,但是S可能还有一些数据需要给C,服务器要确认自己的信息是否发完(TIME_WAIT)。所以S需要一个缓冲时间,无法立刻做出断开通道的决定,这中间是有一个检查时间(缓冲时间)的!

隐喻:

# 三次握手
男:做我女朋友!(建立链接)
女:同意!你也做我男朋友!(同意建立,并发起请求)
男:同意!(同意建立) # 四次挥手
女:分手吧!(不跟你发消息)
男:同意,分手之后做朋友(还跟你发消息)
男:算了吧,还是别联系(断开请求)
女:6 (同意断开)

洪水攻击

洪水攻击:同一时间有大量的客户端请求建立链接 会导致服务端一直处于SYN_RCVD状态
如何抵御洪水攻击?建立半连接池 服务端如何区分客户端建立链接的请求:
1.通过ip
2.通过SYN 如SYN seq=x
x y表示不同客户端、服务端的唯一标识。

UDP协议

# UDP协议
丢包协议、不可靠协议
彼此之间不做任何的链接 不做通道 不二次做确认操作
特点:不需要建立双向通道 数据的传输速度快 但是可能会丢失 qq使用的就是UDP协议 所有可能会丢失
但是我们可以在UDP协议的基础之上做很多额外的扩展来保证数据的安全 '''
TCP协议类似于打电话:你一句我一句 你侬我侬
UDP协议类似于发短信:发了之后不管你看不看 只要发了就行
'''

socket模块

# 编写一个CS架构的程序 实现数据交互
思考:
需要编写代码 操作OSI七层 相当复杂!
就跟操作系统 编写操作硬件的代码一样
由于操作OSI七层 是所有CS架构的程序都需要经历的过程 所以为了方便 产生了socket技术。
在OSI七层中增加了socket抽象层,用socket提供的快捷方式操作其他层

python中socket模块>>>为操作OSI七层协议提供了快捷方式 不需要自己处理一遍

socket也叫套接字,有以下两种分类:
基于文件类型的套接字家族(单机)
AF_UNIX
基于网络类型的套接字家族(联网)
AF_INET

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.socket()  # 会产生一个sockct对象
# 默认情况下socket对象 ---> TCP + 网络型套接字
family = AF_INET # AF_INET的意思就是 基于网络的socket
type = SOCK_STREAM # SOCK_STREAM 流式协议 也就是TCP协议 # SOCK_DGRAM UDP协议
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 实现UDP协议

server.bind()

查看源码:

server.bind(('127.0.0.1', 8080))
'''
bind方法用于给服务端绑定一个固定的地址
接受一个元祖 (IP地址,端口号)
'''

server.accept()

sock, addr = server.accept()  # return sock, addr
# 示例 <socket.socket fd=600, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.1.81', 8080)
'''
accept有两个返回值:
sock表示客户端和服务端之间的管道 可以使用sock对象发送信息等
addr是访问服务端的客户端IP地址 addr是给你打电话的人的地址
sock是电话的听筒 你可以基于听筒跟别人交流! 如果没有客人上门 accept这行代码会一直等待 程序会卡在这里(listen)。
'''

sock.recv() sock.send()

sock.recv(1024)  # 接受
sock.send() # 发送
'''
recv方法用于接收客户端发送过来的消息 如果 没有消息 或者 消息为空 程序都会卡这行代码
这里的数字1024 表示接受1024个字节 多了就抛弃
注意发送和接受的都是bytes类型 要对输入进行转码
'''

sock.close() server.close()

sock.close()  # 关闭通道
server.close() # 关闭服务端
这两行代码也可以不写 因为socket模块有自动关闭的操作。

client.connect()

client.connect(('127.0.0.1',8080))
'''
connect方法用于服务端链接客户端
传入一个元祖:(要访问的服务端IP,服务端端口号)'''

问题和优化

1.聊天内容自定义

针对消息采用input获取

2.让聊天循环起来

将聊天的部分用循环包起来

用户输入的消息不能为空

本质其实是两边不能都是recv或者send 一定是一方收一方发

相当于两边电话通了,但是都不说话,都在等着别人说话= =

可以在服务端加限制,禁止传入空字符串。

服务端多次重启报错

Address already in use 主要是mac电脑会报

解决1:改C/S双方使用的端口号

解决2:加代码

添加如下红框代码:

#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)

客户端异常断开

当客户端异常断开的情况下 如何让服务端继续服务其他客人?

客户端如果异常断开 服务端代码应该重新回到accept等待新的客人!

如果是windows电脑 停掉客户端 服务端会直接报错

如果是mac、linux则不会报错 服务端会接受到一个空消息。

所以对mac、linux:

如果是window,则需要异常处理,捕获到因为服务端断掉而产生的报错:

但是服务端也无法继续服务客户了,还是没回到accept,所有只能再套一个循环:

半连接池

当有多个客户端来链接的情况下 我们可以设置等待数量(不考虑并发问题)

server.listen

server.listen(5)
'''
用于构造半连接池
比如海底捞排队 给门外摆了5把椅子
'''

示例

使得一个py文件可以运行多次:

让多个服务端和客户端产生链接:

绿色的框内有:1个正在被服务的客人 5个在椅子上等待的客人

红色框:2个被拒之门外的客人

如果在报错之前 有客人退出 则会有椅子空出 也就能再坐一个人。

练习

UDP单向通信

# 发送方
import socket client = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
client.bind(('192.168.1.81', 8084)) # 发送方绑定端口 避免随机分配
while True: # 192.168.1.81是本机地址
user_input = input('请输入>>>:')
client.sendto(user_input.encode('utf8'), ('192.168.1.81', 8080)) # 循环给8080端口发送信息 # sendto(bytes类型数据,目标地址元祖)
if user_input == 'q':
break
# 接收方
import socket server = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
server.bind(('192.168.1.81', 8080)) # 不能重复绑定 while True:
date, addr = server.recvfrom(1024) # recvform返回(byte类型数据,发送方ip地址的元祖)
print(f'来自:{addr}的信息>>>:{date.decode("utf8")}')

模拟QQ聊天

首先对于客户端,在不使用多进程的情况下,无法做到一个py文件既发送信息,又接受信息。因为程序的循环会阻塞在input处等待输入,实现不了及时聊天的效果。

推导1、那么可以使用两个py文件,发送.py都使用UDP协议:

这样无论是客户1\客户2的接受.py文件都可以有及时聊天的效果。

推导2、中转

在服务端可以产生及时聊天的效果。

ps:主要还是单进程实现不了又输入又接受的操作,因为不论是recv还是input都是阻塞的,所以想实现QQ聊天只能使用多进程。

总结

window下:

对于python的socket模块:

如果是建立TCP链接,没有server.listen会报错:

OSError: [WinError 10022] 提供了一个无效的参数。

没有server.accept就产生不了管道,也会报错。所以使用socket模块创建TCP链接listen和accept缺一不可。

如果是建立UDP通信则只需要指定你要发送的地址即可。

建立tcp、udp链接时,可以不进行bind绑定,如果这样系统就会给你当前进程,随机分配一个端口号。

又回来总结

注意:
accept 等待的是一个新的进程
sock 是跟某个进程建立的管道 对于这行代码:
with open('inf.txt', 'rb') as f:
for line in f:
client.send(line)
将txt文件用rb二进制模式打开,直接用send方法发送时,此时发送的bytes是16进制的,在接收时无法直接decode('utf8')
需要使用hex()方法:
with open('info2.txt', 'wb') as f:
data = sock.recv(1024).hex() # 将16进制bytes b'\xaa'转换成utf-8 str 'aa'
f.write(data.encode('utf8'))
# 用rb模式打开文件 得到的是16进制的bytes类型 这种类型是不能用decode('utf8')解码的
# 可以用hex方法,将16进制bytes b'\xaa'转换成utf-8 str 'aa'
# bytes类型内部有哪些分类?

参考:

https://blog.csdn.net/zbb19/article/details/123991426

https://www.cnblogs.com/oreoz/p/16892930.html

https://www.cnblogs.com/zkz0206/p/16897399.html

TCP\UDP协议 socket模块的相关教程结束。

《TCP\UDP协议 socket模块.doc》

下载本文的Word格式文档,以方便收藏与打印。