网络编程概念
两大开发架构
C/S B/S
ip地址——互联网协议地址,为互联网上的每一个网络和每一台主机分配一个逻辑地址
以此来屏蔽物理地址的差异。它是一个32位的二进制数
mac地址
内网/外网
交换机:单播,广播,组播
在一个局域网内的通信
二层/数据链路层 - osi七层协议
数据链路层的协议:arp协议(通过一台机器的ip地址找到mac地址)
数据链路层的网络设备:交换机、网卡
路由器:
局域网与局域网之间的连接工具
三层/网络层 - osi七层协议
网络层的协议:ip协议
网络层的设备:路由器、三层交换机
网关:
一个局域网要想访问局域网外部的其他机器,都要通过统一的网关接口
子网掩码
判断两台机器是否在同一个局域网内
端口
定位某一台机器上的某一个服务,可认为是设备与外界通讯交流的出口
如何定位你的网络中能够找到的唯一的一台机器+服务?
ip+服务端口号
ip地址精确到具体的一台电脑
端口精确到具体的程序
osi七层协议的传输层:tcp/udp协议
这两个协议标志着我们的数据传递的方式都要基于网络传输
tcp协议(好比是语音通话)
必须先接通电话,同一时刻只能和一个人聊天,传输比较稳定
可以发送任意长度的消息
udp协议(发微信)
不需要接通,只要互相知道ip,端口就可以通信
同时可以和多个人聊天,传输速度快,但是传输相对不稳定,不能传输过长的消息
tcp:发送文件(邮件)、下载安装包、上传下载电影、从网盘上传、下载文件
udp: qq、微信、小视频软件等,即时通信类
tcp协议
为什么传输稳定、可靠
先建立连接,然后三次握手
1. 就好比qq给服务器发送一个值1200,问它,“我可以和你通话吗?”
2. 这时服务器回答,“可以”,并且返回给qq一个值1201来表示接收到了
然后服务器要问qq,“我可以和你通话吗?”
3. qq回复“可以”
这就是建立连接的三次握手。注意,服务器发送1201和问qq归于一次握手
数据传输
1. qq给服务器发送一条消息
2. 服务器收到后给qq返回一条消息
这两个过程合在一起才算一个完整的数据传输
断线连接四次挥手
1. qq跟服务器说,“我不想和你说话了”
2. 服务器接收到后,说,“好的”
3. 服务器也说,“我也不想和你说话了”
4. qq回答“好的”
注意2和3不能一起发,因为qq说不想和服务器说话,但是服务器还要和qq说话
tcp协议与udp协议的流程图:
socket层
所处位置:在应用层和下面所有层之间的位置
有了socket可以以相对简单的方式进行网络通信
本质上是帮助我们解决两个程序之间通信
socket是应用层与tcp/ip协议通信的中间软件抽象层,是一组接口
下面开始举例加深对 socket 的理解 建立一个文件夹
比如“tcp协议的socket” 再在里面建立两个文件,分别命名为“server.py”和“client.py”
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
# server.py import socket # 套接字 sk = socket.socket() sk.bind(("127.0.0.1", 8080)) # 把地址绑定到套接字 # 元组的第一个元素是字符串形式的ip地址,必须是本电脑的。 # ip地址两种 # 192.168.13.4 (在cmd里面输入 ipconfig 即可查询) # 还有一个 “127.0.0.1”,本地回环地址 # 这两个ip地址不一样 # 192.168.... 所有的和我在同一个局域网的小伙伴都能访问 # 127.0.0.1 只有在自己电脑的client才能用 # 第二个元素是端口号,一般在8000-10000之间 # 三次握手 sk.listen() # 监听链接 conn, addr = sk.accept() # 阻塞,直到有一个客户端来连接 # print(addr) conn.send(b"hello") msg = conn.recv(1024) # 最多接收1024个字节,没1024时有多少接收多少 print(msg) # 四次挥手 conn.close() # 关闭客户端套接字 sk.close() # 关闭服务器套接字
# client.py import socket sk = socket.socket() # 创建客户端套接字 sk.connect(("127.0.0.1", 8080)) # 把三次握手建立起来了,尝试连接服务器 # 这里connect一次,上面(server.py)就accept一次 msg = sk.recv(1024) # 对话(发送/接收) print(msg) sk.send(b"Goodgoodstudy,Daydayup!") sk.close() # 关闭客户端套接字
# 然后先运行 server.py, 再运行 client.py,两个都会有结果
# 注意收发次数要相等,还要一一对应
输入中文需要编码解码
这里涉及到编码问题
str 字符串数据类型
bytes 字节数据类型
中:gbk bytes: 100100110101
中:uft-8 bytes: 100111100001
send——一定是bytes类型
bytes = recv() 也是bytes类型
我们看,即运行结果要显示的话,是看str的
我们在发送数据之前是str, 发送的是bytes
就需要对str进行编码,str.encode("utf-8"),结果是一个bytes
我们在接收数据的时候,收到的也是bytes
要想看懂必须把bytes解码,bytes.decode("utf-8"),这样结果就变成字符串了
用一张图来显示:
# 服务端输入中文时编码,客户端那边要解码,反之亦反 # server.py import socket # 套接字 sk = socket.socket() sk.bind(("127.0.0.1", 8080)) sk.listen() conn, addr = sk.accept() # 给客户端发送中文时需编码 conn.send("你好".encode()) # 注意在send里面直接编码发送出去 recv_msg = conn.recv(1024) decode_recv_msg = recv_msg.decode("utf-8") # 接收客户端发来的反馈,要解码 print(decode_recv_msg) conn.close() sk.close()
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 8080)) # 接收服务端发来的信息 msg = sk.recv(1024) decode_msg = msg.decode() # 接收上面服务端发来的消息,要解码 print(decode_msg) # 给服务端发送信息 sk.send("好好学习~".encode()) # 发送给服务端的消息,send里面编码 sk.close()
带退出的聊天程序
# server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 8800)) sk.listen() conn, addr = sk.accept()
# 注意在 accept之后加上循环
# 是为了能够让我们和一个客户端多说几句话 while 1: send_msg = input("msg: ") # q conn.send(send_msg.encode()) # send(q) if send_msg == "q": break msg = conn.recv(1024).decode() if msg == "q": break print(msg) conn.close() sk.close()
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 8800)) while 1: msg = sk.recv(1024).decode() if msg == "q": break print(msg) send_msg = input("msg: ") sk.send(send_msg.encode()) if msg == "q": break sk.close()
小练习
所有的client端都要以server端的时间为基准
client端发送一个时间格式——"%Y-%m-%d %H:%M:%S"
server端根据接收到的时间格式向客户端返回时间
# server.py import socket # 套接字 import time sk = socket.socket() sk.bind(("127.0.0.1", 9000)) sk.listen() conn, addr = sk.accept() msg = conn.recv(1024) print(msg) format_msg = time.strftime(msg.decode()) conn.send(format_msg.encode()) conn.close() sk.close()
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 9000)) sk.send(b"%Y-%m-%d %H:%M:%S") msg = sk.recv(1024).decode() print(msg) sk.close()
让server同时接收多个client请求的方法
# server.py import socket # 套接字 import time sk = socket.socket() sk.bind(("127.0.0.1", 9000)) sk.listen() # 在 accept之前加上循环,能够让我们和多个客户端进行沟通 # 注意与上面循环的区别 while 1: conn, addr = sk.accept() # 接收一个客户端的请求 print(addr) msg = conn.recv(1024) # 和这个接通的客户端进行通信 fmt = time.strftime(msg.decode()) print(msg) conn.send(fmt.encode()) conn.close() # 断开一个客户端的请求 sk.close() # client.py内容不变,先运行本文件 # 然后一直重复运行client.py文件 # 运行结果会刷新,不会报错
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 9000)) sk.send(b"%Y-%m-%d %H:%M:%S") msg = sk.recv(1024).decode() print(msg) sk.close()
粘包
粘包现象的本质
接收端不知道发送端给它发送了多长的数据
注意:只有tcp有粘包现象,udp永远不会粘包
但是udp会丢失数据,不可靠
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住
而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
# 了解粘包之前,先了解 struct 模块的用法 import struct # pack:固定4个字节 ret = struct.pack("i", 5000000) print(ret, len(ret)) ret1 = struct.pack("i", 123) print(ret1, len(ret1)) ret2 = struct.pack("i", 902730757) # 不能超过2**32个字节 print(ret2, len(ret2)) # b'@KL\x00' 4 # b'{\x00\x00\x00' 4 # b'\x05\x94\xce5' 4 # unpack: res = struct.unpack("i", ret) print(res[0]) res1 = struct.unpack("i", ret1) print(res1[0]) res2 = struct.unpack("i", ret2) print(res2[0]) # 5000000 # 123 # 902730757
# server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 9001)) sk.listen() conn,addr = sk.accept() conn.send(b"hello") # 粘包现象 conn.send(b"word") conn.close() sk.close()
# 设置时间延迟来观察粘包现象
import socket import time sk = socket.socket() sk.connect(("127.0.0.1", 9001)) time.sleep(0.1) msg = sk.recv(1024) print(msg) msg2 = sk.recv(1024) print(msg2) # b'helloword' # b''
粘包现象
# 1.发送端的粘包 合包机制 + 缓存区
发送方引起的粘包是由TCP协议本身造成的
TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段
若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段
然后一次发送出去,这样接收方就收到了粘包数据
也就是说,发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据又很小,会合到一起,产生粘包)
# 2.接收端的粘包 延迟接受 + 缓存区
接收方不及时接收缓冲区的包,造成多个包接收
客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包
# 3.流式传输 # 电流 高低电压 # 所以我们说 tcp协议是无边界的流式传输 # 4.拆包机制
总结
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
# 可以通过 struct 模块来解决粘包问题
问题的根源在于,接收端不知道发送端将要传送的字节流的长度
解决办法是,可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节
这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小
那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
# server.py import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() conn,addr = sk.accept() send_msg = input('>>>').encode() bytes_len = struct.pack('i',len(send_msg)) # 注意这里是用len()来代表字节长度 conn.send(bytes_len) conn.send(send_msg) conn.send(b'world') conn.close() sk.close()
# client.py import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',9000)) bytes_len = sk.recv(4) # 这里接收到的是一个元组,因此要设置索引来得到想要的结果 msg_len = struct.unpack('i',bytes_len)[0] msg = sk.recv(msg_len) print(msg.decode()) msg2 = sk.recv(5) print(msg2) sk.close()