赞
踩
前情回顾 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.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 传输流程
发送端由应用程序发送消息,上到下逐层添加首部信息,最终在物理层发送消息包。如果数据过大,拆分成一帧一帧发送,帧头与帧尾将这大数据按照顺序连接起来。
发送的消息经过多个网络设备节点(交换机,路由器)传输,最终到达目标主机。
目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。
编辑
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时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。