前情回顾 1. 网络概念 什么是网络 : 数据传输 OSI七层模型 : 网络通信标准化流程模型 tcp/ip模型 : 实际工作模型 网络协议 : 网络通信过程中都遵循的规定 网络地址 : IP(公网,内网) 端口 服务端客户端: C/S结构 B/S cookie: 面试理论问题 这是什么->具体描述->特点(优点,缺点)->话题引申->我用它干什么 2. 套接字编程 (写功能,确定地址,选择服务) UDP套接字 (也叫数据报套接字) (udp协议) 服务端: socket()->bind()->recvfrom/sendto->close() 客户端: socket()--------->sendto/recvfrom->close() 训练1: 改写代码,让客户端可以循环收发消息, 直到输入##两边都退出 注意:1.address error 当非法中断服务端端口,而不是##程序中断时候,系统认为网络断开 与服务连接不上,此时重新运行服务端会报端口地址重复错误,过几分钟就好了。 操作系统自动将这个地址丢弃。 2.broken pipe ereror --在客户端不通知服务端的情况下退出(自己非法结束终端), 那么服务端的recv不在阻塞了,他会返回一个空字节串,然后打印空,发送thanks, 再循环一遍,再返回空,再打印空,然后就不发thanks了,开始报错。 3.连接就意味责任,一方退出必须告诉对方,否者报错。 解决方法:服务端写上if not data or data == b"##",python的客户端是 无法发送空字节串。 训练2: 改写代码,让客户端退出但是服务端不退出, 服务端可以继续处理下一个客户端连接 练习01: 完成一个图片上传练习,将一个图片从客户端 上传到服务端,在服务端以 20210409.jpg为 名字保存 图片自选 注意,图片可能比较大,不建议一次性read读取 练习02: 模拟一个问答机器人 从客户端输入问题发送给服务端, 服务端根据问题中是否有关键字返回对应的回答 你多大了 : 我今年2岁了 男生女生 : 我是机器人 叫什么 : 我叫小美 人家还小不知道啦 作业 : 1. 今天的重点代码 自己独立完成 \2. 两天的练习自己没写出来的 独立完成
1.3 TCP 传输方法
1.3.1 TCP传输特点
-
面向连接的传输服务:即传输之前必须建立连接关系
-
传输特征 : 提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复。
-
可靠性保障机制(都是操作系统网络服务自动帮应用完成的):
-
在通信前需要建立数据连接
-
确认应答机制
-
通信结束要正常断开连接
-
-
-
三次握手(建立连接)
-
客户端向服务器发送消息报文请求连接
-
服务器收到请求后,回复报文确定可以连接
-
客户端收到回复,发送最终报文连接建立
-

编辑
-
四次挥手(断开连接)
-
主动方发送报文请求断开连接(谁先发起都可以)
-
被动方收到请求后,立即回复报文,表示准备断开
-
被动方准备就绪,再次发送报文表示可以断开
-
主动方收到确定,发送最终报文完成断开
-

编辑
1.3.2 TCP服务端处理

编辑
服务端通信流程:创建TCP套接字->绑定自己服务端地址->具备监听功能,可以被客户端连接listen->阻塞等待客户端连接accept->收发消息->关闭套接字。因此对比于udp,多了listen与accept是处理连接三次握手用的。
-
创建套接字
sockfd=socket.socket(family,type) 功能:创建套接字 参数:family 网络地址类型 AF_INET表示ipv4 type 套接字类型 SOCK_STREAM默认,表示tcp套接字 (也叫流式套接字) 返回值: 套接字对象
-
绑定地址 (与udp套接字相同)
-
设置监听(使得具备被客户端连接的功能)
sockfd.listen(n) 功能 : 将套接字设置为监听套接字,确定监听队列大小,即具备被客户端连接能力。实现缓冲效果。 参数 : 监听队列大小(只能一个一个处理客户端,给他一个队列排队等待) 在Linux下这个参数就是摆设,操作系统自动帮你设置监听队列缓冲区大小,设置越大占内存越大,并且三次握手几乎在瞬间完成。

编辑
-
处理客户端连接请求
connfd,addr = sockfd.accept() 功能: 阻塞等待处理客户端请求 返回值: connfd 客户端连接套接字 addr 连接的客户端地址

![]()
服务端accept阻塞等待客户端发送数据建立连接关系后,为每一个客户端单独创建一个connfd套接字,应对每一个客户端的数据传输功能,而socket专门用于建立连接,连接完成一个客户端创建一个connfd,他是客户端的专属服务管家。如果客户端退出了,对应的connfd随之销毁。
-
消息收发
data = connfd.recv(buffersize) 功能 : 接受客户端消息 参数 :每次最多接收消息的大小 返回值: 接收到的内容 n = connfd.send(data) 功能 : 发送消息 参数 :要发送的内容 bytes格式 返回值: 发送的字节数
-
关闭套接字 (与udp套接字相同)
"""
tcp服务端数据传输案例:重点代码
"""
from socket import *
# 1.创建tcp套接字
tcp_socket = socket(AF_INET, SOCK_STREAM) # 默认选择也是TCP协议
# 2.绑定地址:只有服务端才需要固定地址
tcp_socket.bind(("0.0.0.0", 8888))
# 3.设置为监听功能:同一时刻允许5个客户端发起连接给他一个缓冲队列等待
tcp_socket.listen(5)
# 4.阻塞等待客户端处理连接
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr) #发现客户端需要与我建立连接完成
# 5.先收后发消息
data = connfd.recv(1024) # 阻塞等待接收数据:一次连接接收数据后就断开,tcp不循环接收数据照样丢失
print("服务端收到数据为:", data.decode())
connfd.send(b"Thinks")
# 6.关闭
connfd.close() # 关闭与某个客户端连接:完成四次挥手
tcp_socket.close() # 关闭套接字,断开所有客户端
1.3.3 TCP客户端处理

![]()
创建与服务端相同类型的套接字->客户端发起连接->数据的发送与接受->关闭套接字
-
创建TCP套接字
-
请求连接
sockfd.connect(server_addr) 功能:连接服务器 参数:元组 服务器地址
-
收发消息
注意: 防止两端都阻塞,recv send要配合用
-
关闭套接字
"""
tcp客户端数据传输案例
"""
from socket import *
# 1.创建与服务端相同tcp套接字
tcp_socket = socket(AF_INET, SOCK_STREAM) # 默认选择也是TCP协议
# 2.请求连接,三次握手,与服务端accept对应:写服务器地址
tcp_socket.connect(("127.0.0.1", 8888))
# 3.先发后收消息
tcp_socket.send(b"hello")
data = tcp_socket.recv(1024) # 阻塞等待接收数据
print("客户端收到数据为:", data.decode())
# 4.关闭tcp套接字
tcp_socket.close()

编辑
"""
改写代码,让客户端可以循环收发消息,
直到输入##两边都退出
"""
from socket import *
from time import sleep
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr)
while True:
data = connfd.recv(1024) # 阻塞等待接收数据
# 当连接的两端有一端(通常是客户端)突然退出的时候
# 另外一端的recv就不再阻塞,会得到一个空字节串
if data.decode() == "##" or not data:
break
print("服务端收到数据为:", data.decode())
connfd.send(b"Thinks")
# sleep(0.1)
connfd.close() # 关闭与某个客户端连接:完成四次挥手
tcp_socket.close() # 关闭套接字,断开所有客户端
"""客户端程序"""
from socket import *
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
while True:
senddata = input(">>")
tcp_socket.send(senddata.encode())#如果发送##通知服务端要断开连接
if senddata == "##":#不然客户端不打招呼给服务端就退出容易管道破裂
break#客户端发不了空字节串,因为发他没有意义
data = tcp_socket.recv(1024) # 阻塞等待接收数据
print("客户端收到数据为:", data.decode())
tcp_socket.close()
"""
改写代码,让客户端退出但是服务端不退出,
服务端可以继续处理下一个客户端连接
这里程序是循环模型,不是并发模型,不能应对多个客户端同时发出数据做处理,只能在
这个客户端处理完成退出后,才能等待下一个客户端连接。即tcp 长连接形态
udp也是循环模型,虽然任何人发数据都接收,但只是处理快的让你感觉多个客户端
可以同时处理
"""
from socket import *
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
while True:
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr)
while True:
data = connfd.recv(1024) # 阻塞等待接收数据
if data.decode() == "##" or not data:
break
print("服务端收到数据为:", data.decode())
connfd.send(b"Thinks")
connfd.close()
"""
客户端程序
"""
from socket import *
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
while True:
senddata = input(">>")
tcp_socket.send(senddata.encode())
if senddata == "##":
break
data = tcp_socket.recv(1024) # 阻塞等待接收数据
print("客户端收到数据为:", data.decode())
tcp_socket.close()
"""
完成一个图片上传练习,将一个图片从客户端
上传到服务端,在服务端以 20210409.jpg为名字保存
图片自选 注意,图片可能比较大,不建议一次性read读取
"""
from socket import *
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
file1 = open("caihua.jpeg", "rb")
while True:
data = file1.read(1024)
if not data:
break
tcp_socket.send(data)
tcp_socket.close()
#发送完成客户端关闭,客户端退出后服务端收到系统发的空字符串
"""
服务端程序
"""
"""
完成一个图片上传练习,将一个图片从客户端
上传到服务端,在服务端以 20210409.jpg为名字保存
图片自选 注意,图片可能比较大,不建议一次性read读取
"""
from socket import *
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
file2 = open("20210402.jpg", "wb")
while True:
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr)
while True:
data = connfd.recv(1024) # 阻塞等待接收数据
file2.write(data)
if not data:
break
connfd.close()
file2.close()
"""
升级
tcp服务端例子:短连接形态,看起来好像和udp一样同一时间可以应对多个客户端
其实并不是,只是应对的客户端只接收4字节后就迅速断开,不是真正的同时
即只处理一次数据交互就断开连接。如果下次还想进行交互就必须建立连接
之前是一次只应对一个客户端,一次连接长久有效,效率较高
现在是不利用并发技术下 一次应对多个客户端,由于每次需要三次挥手,效率较低
"""
from socket import *
def main():
tcp_sock = socket()
tcp_sock.bind(("0.0.0.0", 8888))
tcp_sock.listen(5)
while True:
connfd, add = tcp_sock.accept()
print("connect from:", add)
handle(connfd) # 只进行一次客户端交互数据就断开,不能长期占有服务器端口代码
connfd.close()
def handle(connfd):
data = connfd.recv(4)
print(data.decode())
connfd.send(b"ok")
if __name__ == '__main__':
main()
"""
客户端1
"""
from socket import *
def main():
while True:
msg = input(">>")
if not msg:
break
tcp_connect(msg)
def tcp_connect(msg):
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("客户端收到数据为:", data.decode())
tcp_socket.close()
if __name__ == '__main__':
main()
"""
客户端2:如果服务端1的数据传输需要占用很长时间,那么客户端2也连接不上服务端。因此这个循环模型适合处理小数据传输,传输完成后立即等待下一个客户端连接。
"""
from socket import *
def main():
while True:
msg = input(">>")
if not msg:
break
tcp_connect(msg)
def tcp_connect(msg):
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("客户端收到数据为:", data.decode())
tcp_socket.close()
if __name__ == '__main__':
main()
"""
练习02: 模拟一个问答机器人
从客户端输入问题发送给服务端,
服务端根据问题中是否有关键字返回对应的回答
客户端2
你多大了 : 2ages
客户端3:
男生女生 : robort
客户端1
叫姓名什么 : xiao yi
"""
from socket import *
def main():
tcp_sock = socket()
tcp_sock.bind(("0.0.0.0", 8888))
tcp_sock.listen(5)
while True:
connfd, add = tcp_sock.accept()
print("connect from:", add)
handle(connfd)
connfd.close()
def handle(connfd):
data = connfd.recv(1024)
if "age" in data.decode():
connfd.send(b"2 ages")
elif "name" in data.decode():
connfd.send(b"xiao yi")
elif "sex" in data.decode():
connfd.send(b"robot")
else:
connfd.send(b"sorry I not know")
if __name__ == '__main__':
main()
"""客户端1"""
from socket import *
def main():
while True:
msg = input(">>")
if not msg:
break
tcp_connect(msg)
def tcp_connect(msg):
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("客户端1收到数据为:", data.decode())
tcp_socket.close()
if __name__ == '__main__':
main()
"""
客户端2
"""
from socket import *
def main():
while True:
msg = input(">>")
if not msg:
break
tcp_connect(msg)
def tcp_connect(msg):
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("客户端2收到数据为:", data.decode())
tcp_socket.close()
if __name__ == '__main__':
main()
1.3.4 TCP套接字细节
-
tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会收到一个空字串。
-
tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError
-
一个服务端可以同时连接多个客户端,也能够重复被连接,即循环模型的短连接形态
-
tcp粘包问题
-
产生原因
-
为了解决数据再传输过程中可能产生的速度不协调问题,例如发送端发10个字节,接收端按照5个字节接收。如果不是长连接模型那么另外的5个字节就丢了,但操作系统设置了缓冲区专门给tcp提供可靠不丢失传输服务,分两次发送。
-
实际网络工作过程比较复杂,导致消息收发速度不一致
-
tcp以字节流方式进行数据传输,udp是数据报方式传输,在接收时不区分消息边界,发送端发送的数据都连接堆积在缓冲区里面,如同水流不间断。
-

编辑
-
-
带来的影响
-
如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
-
-
处理方法
-
消息格式化处理,如人为的添加消息边界,用作消息之间的分割。
-
软件sleep来控制发送的速度
-
-
from socket import *
from time import sleep
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
while True:
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr)
while True:
data = connfd.recv(2) # 阻塞等待接收数据
if data.decode() == "##" or not data:
break
print("服务端收到数据为:", data.decode())
connfd.send(b"Thinks#")#区分粘包边界
sleep(0.1)#等完全发出去Thinks后再接收服务端缓冲区剩余字节
connfd.close()
1.3.5 TCP与UDP对比
-
传输特征
-
TCP提供可靠的数据传输,但是UDP则不保证传输的可靠性
-
TCP传输数据处理为字节流,而UDP处理为数据包形式
-
TCP传输需要建立连接才能进行数据传,效率相对较低,UDP比较自由,无需连接,效率较高
-
-
套接字编程区别
-
创建的套接字类型不同
-
tcp套接字会有粘包,udp套接字有消息边界不会粘包
-
tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
-
tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom
-
-
使用场景
-
tcp更适合对准确性要求高,传输数据较大的场景
-
文件传输:如下载电影,访问网页,上传照片
-
邮件收发
-
点对点数据传输:如点对点聊天,登录请求,远程访问,发红包
-
-
udp更适合对可靠性要求没有那么高,传输方式比较自由的场景
-
视频流的传输: 如直播,视频聊天
-
广播:如网络广播,群发消息
-
实时传输:如游戏画面
-
-
在一个大型的项目中,可能既涉及到TCP网络又有UDP网络
-
1.4 数据传输过程
1.4.1 传输流程
-
发送端由应用程序发送消息,上到下逐层添加首部信息,最终在物理层发送消息包。如果数据过大,拆分成一帧一帧发送,帧头与帧尾将这大数据按照顺序连接起来。
-
发送的消息经过多个网络设备节点(交换机,路由器)传输,最终到达目标主机。
-
目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。

编辑
1.4.2 传输层的TCP协议首部信息(了解)

编辑
-
源端口和目的端口 各占2个字节,分别写入源端口和目的端口。
-
序号seq占4字节。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号是301,而接待的数据共有100字节。这就表明本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。
-
确认号ack占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
-
确认ACK(ACKnowledgment) 仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。
-
同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。
-
终止FIN(FINis,意思是“完”“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
本文深入讲解TCP与UDP两种网络协议的特点及应用场景,包括TCP的三次握手、四次挥手过程,以及UDP的数据报传输方式。同时介绍了套接字编程的具体实现,并通过实例演示了如何使用这两种协议进行数据传输。

1万+

被折叠的 条评论
为什么被折叠?



