举个例子,在大学校园,找到名叫Lucy的男神可以用学号,脱离了校园,你要找到Lucy男神,你就要通过身份证号,这里学号是临时的编号,变化的,出了校园就不可用了;身份证号是唯一标识,不变的,在哪里都能找到。
在计算机中,也有类似的概念,不变的是mac地址,能够唯一标识你这台机器;变化的是ip地址,能够更方便的找到你的机器。(可以类比生活中运送快递)
ipv4协议:规定ip地址是由四位点分十进制数组成,每一位都是八位二进制(表示的范围是0 ~ 255),所以ip地址范围0.0.0.0 ~ 255.255.255.255
公网地址:需要我们自己申请购买的地址
内网地址:保留字段,192.168.0.0 ~ 192.168.255.255 和 172.16.0.0 ~ 172.31.255.255 和 10.0.0.0 ~ 10.255.255.255,在这些范围内的地址都是内网地址。
访问公网地址用的是路由器,访问内网地址用的是交换机。
有一个特殊的ip地址,127.0.0.1 本地回环地址。一般都是做测试的时候使用。
查看自己的ip地址:在cmd中输入ipconfig查看。linux和mac通过ifconfig。
比如,172.168.14.0这个网段内,最多能有254台机器,还有一台是网关ip。因为ip地址中0用于指定网络地址号,192.168.14.0代表的是网段,而不是IP。
子网掩码:也是一个ip地址,用来判断两台机器在不在一个局域网内。用其中一个ip地址和子网掩码进行按位与(先把每位都转成二进制,然后再按位与),在用另外一个ip地址和子网掩码进行按位与,然后比较两次按位与后的ip地址是否一样,如果一样,说明在同一个局域网,反之,不在同一个局域网。
比如192.168.12.1和192.168.13.1,前面只有2位一样(192.168),所以只需2个255,即子网掩码是255.255.0.0,能说明这两个ip在同一个局域网。
有两台机器,分别是192.168.12.1和192.168.13.1,如何判断这两台地址在同一个网段内 子网掩码是255.255.255.0-->11111111.11111111.11111111.00000000 192.168.12.1-->11000000.10101000.00000110.00000001 按位与后是11000000.10101000.00000110.00000000-->192.168.12.0 192.168.13.1-->11000000.10101000.00000111.00000001 按位与后是11000000.10101000.00000111.00000000-->192.168.13.0 按位与后的两个地址不同,说明不在一个局域网,反之,在同一个局域网。 上面例子说明不在同一个局域网。
在网卡上,唯一。
arp协议:地址解析协议。通过一台机器的ip地址获取到它的mac地址,用到了交换机的广播和单播功能。
端口
端口的范围:0 ~ 65535,用来确认机器上的具体应用程序的。
连在同一台交换机上的多台机器形成了局域网络,所有的消息传递,都是交给交换机来处理。交换机只识别mac地址。
我知道一台机器的ip地址,然后要给它发消息,由于交换机不认识ip地址,所以要先获取到这台机器的mac地址,然后双方都知道对方的mac地址,这样用交换机进行数据的传递就非常方便了。
交换机:广播、单播、组播
网段:192.168.12.XX
需要借助路由器。路由器认识ip地址。路由器提供网关ip,同一个局域网的所有机器共享一个网关,我们不能访问除了本局域网之外的其他内网的ip地址。
网关:一个网络连接到另一个网络的“关口”。
import socket # 套接字 sk = socket.socket() # 创建一个server端的对象 sk.bind(('127.0.0.1', 9000)) # 给server端绑定一个地址 sk.listen() # 开始监听客户端给我的连接 conn, adder = sk.accept() # 建立连接,conn是连接 # 服务端 发送 消息给客户端 conn.send(b'hello') # 服务端 接收 来自客户端的消息 msg = conn.recv(1024) print(msg) conn.close() # 关闭连接 sk.close()
import socket sk = socket.socket() # 必须和server端的一致 sk.connect(('127.0.0.1', 9000)) msg = sk.recv(1024) print(msg) # 客户端 发送 消息给服务端 sk.send(b'bye') sk.close()
七层协议:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
应用层:python代码,b’hello’
传输层:预备如何传输、使用的端口,b’发送者的端口+hello+接受者的端口’
网络层:ip,b’发送者的ip+发送者的端口+hello+接受者的端口+接受者的端口’
数据链路层:mac,b’发送者mac地址+发送者的ip+发送者的端口+hello+接受者的端口+接受者的端口+接受者mac地址’
物理层:转化成电信号,通过网线传输
第五层 应用层:python代码。
第四层 传输层:port,udp,tcp。四层路由器,四层交换机。
第三层 网络层:ipv4和ipv6。路由器,三层交换机。(家里的路由器带有交换机功能)
第二层 数据链路层:mac,arp协议。网卡,二层交换机。
第一层 物理层:网线。
tcp(语音聊天)- 线下缓存高清电影、qq远程控制、发邮件
需要先建立连接,然后才能通信的。
特点:占用连接、可靠(消息不会丢失)、实时性高、慢
建立连接:指三次握手
断开连接:指四次挥手
udp(发短信)- 在线播放视频、qq发消息、微信发消息
不需要建立连接,就可以通信的。
特点:不占用连接、不可靠(消息可能因为网络不稳定而丢失)、快
操作系统会统一分配计算机的所有资源
socket():tcp协议的server
socket(type=socket.SOCK_DGRAM):udp协议的server【udp协议中使用】
bind:绑定一个id和端口
listen:监听,代表socket服务的开启
accept:等到有客户端来访问,与客户端建立连接
send:直接通过连接发送消息,不需要写地址
sendto:需要写一个对方的地址【udp协议中使用】
recv:只接收消息
recvfrom:接收消息和地址【udp协议中使用】
connect:客户端/tcp协议的方法,和server端建立连接
close:关闭服务(sk.close)/连接(conn.close)
server.py
import socket sk = socket.socket() sk.bind(('127.0.0.1', 9000)) # 申请操作系统的资源 sk.listen() while True: # print(f'sk:{sk}') # conn里存储的是一个客户端和服务端的连接信息 conn, adder = sk.accept() # 能够和多个客户端进行握手了 # print(f'conn:{conn}') conn.send(b'hello') msg = conn.recv(1024) print(msg) conn.close() # 挥手,断开连接 sk.close() # 归还申请的操作系统的资源
client.py
import socket sk = socket.socket() # 必须和server端的一致 sk.connect(('127.0.0.1', 9000)) msg = sk.recv(1024) print(msg) # 客户端 发送 消息给服务端 sk.send(b'bye') sk.close()
server.py
import socket sk = socket.socket() sk.bind(('127.0.0.1', 9000)) # 申请操作系统的资源 sk.listen() while True: # 为了和多个客户端进行握手 # conn里存储的是一个客户端和服务端的连接信息 conn, adder = sk.accept() # 能够和多个客户端进行握手了 while True: send_msg = input('please input you want say:') conn.send(send_msg.encode('utf-8')) if send_msg.upper() == 'Q': break msg = conn.recv(1024).decode('utf-8') if msg.upper() == 'Q': break print(msg) conn.close() # 挥手,断开连接 sk.close()
client.py
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: msg = sk.recv(1024).decode('utf-8') if msg.upper() == 'Q': break print(msg) send_msg = input('please input you want say:') sk.send(send_msg.encode('utf-8')) if send_msg.upper() == 'Q': break sk.close()
input() 等待,直到用户输入enter键 accept() 阻塞,有客户端来与我建立完连接之后 recv() 阻塞,直到收到对方发过来的消息之后 recvfrom() 阻塞,直到收到对方发过来的消息之后 connect() 阻塞,直到server端结束了对一个client的服务,开始和当前client建立连接的时候
server.py
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 表示一个udp协议 sk.bind(('127.0.0.1', 9000)) # 服务端不能先发送消息,因为服务端不知道客户端的ip while True: msg, addr = sk.recvfrom(1024) print(msg.decode('utf-8')) send_msg = input('please input you want say:') sk.sendto(send_msg.encode('utf-8'), addr) # server端不需要判断退出 # 因为不和这个客户端通信,还要和其他客户端通信
client.py
import socket sk = socket.socket(type=socket.SOCK_DGRAM) server = ('127.0.0.1', 9000) while True: send_msg = input('please input you want say:') if send_msg.upper() == 'Q': break sk.sendto(send_msg.encode('utf-8'), server) msg = sk.recv(1024).decode('utf-8') if msg.upper() == 'Q': break print(msg)
两条或更多条分开发送的信息连在一起就是粘包现象
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'nice') conn.close() sk.close()
import time import socket sk = socket.socket() sk.connect(('127.0.0.1', 9001)) time.sleep(0.1) msg1 = sk.recv(1024) print(msg1) msg2 = sk.recv(1024) print(msg2) sk.close()
先启动server,然后启动client,client打印结果如下:
b'hellonice' b''
产生了粘包现象,去掉睡眠时间结果如下:
b'hellonice' b'nice'
【注意】即使不设置睡眠时间,也有几率粘包
粘包现象只出现在tcp协议中,因为tcp协议中,多条消息之间没有边界(流式传输),并且还有一大堆优化算法。
tcp协议的传输是流式传输,数据与数据之间没有边界
设置边界
自定义协议
服务端:先发送4个字节的数据长度,再按照长度发送数据
客户端:先接收4个字节,知道数据的长度,再按照长度接收数据
该模块可以把一个类型,如数字,转成固定长度的 bytes。
数字的范围 -2**31-1 ~ 2**31
,共 2**32 个数字。pack可以把这个范围内的任意一个数字转为长度为 4 的字节。
import struct num1 = 123456789 num2 = 12345 num3 = 123 ret1 = struct.pack('i', num1) print(ret1, len(ret1)) # b'\x15\xcd[\x07' 4 ret2 = struct.pack('i', num2) print(ret2, len(ret2)) # b'90\x00\x00' 4 ret3 = struct.pack('i', num3) print(ret3, len(ret3)) # b'{\x00\x00\x00' 4 print(struct.unpack('i', ret1)) # (123456789,) print(struct.unpack('i', ret2)) # (12345,) print(struct.unpack('i', ret3)) # (123,)
import socket import struct sk = socket.socket() sk.bind(('127.0.0.1', 9001)) sk.listen() conn, addr = sk.accept() msg1 = input('>>>') msg2 = input('>>>') blen = struct.pack('i', len(msg1.encode())) conn.send(blen) conn.send(msg1.encode()) conn.send(msg2.encode()) conn.close() sk.close()
import time import socket import struct sk = socket.socket() sk.connect(('127.0.0.1', 9001)) time.sleep(0.1) length = sk.recv(4) length = struct.unpack('i', length)[0] msg1 = sk.recv(length) msg2 = sk.recv(1024) print(msg1.decode('utf-8')) print(msg2.decode('utf-8')) sk.close()
自动识别用户,不能使用 ip 和 port
import socket friend_lst = {'jack': '31', 'tom': '34'} sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(('127.0.0.1', 9000)) while 1: msg, addr = sk.recvfrom(1024) msg = msg.decode('utf-8') name, message = msg.split('|') print(f'\033[1;{friend_lst.get(name,"30")}m {name}:{message} \033[0m') content = input('>>>') sk.sendto(content.encode('utf-8'), addr)
import socket name = 'jack' sk = socket.socket(type=socket.SOCK_DGRAM) while 1: content = input('>>>') if content.upper() == 'Q': break content = f'{name}|{content}' sk.sendto(content.encode('utf-8'), ('127.0.0.1', 9000)) msg = sk.recv(1024).decode('utf-8') if msg.upper() == 'Q': break print(msg)
import socket import json # 接收 sk = socket.socket() sk.bind(('127.0.0.1', 9002)) sk.listen() conn, addr = sk.accept() msg = conn.recv(1024).decode('utf-8') msg = json.loads(msg) with open(msg['filename'], 'wb') as f: content = conn.recv(msg['filesize']) print('-->', len(content)) f.write(content) print(msg) conn.close() sk.close()
import os import json import socket # 发送 sk = socket.socket() sk.connect(('127.0.0.1', 9002)) # 文件名、文件大小 abs_path = r'E:\A.PythonProject\action\day30\作业\a.txt' filename = os.path.basename(abs_path) filesize = os.path.getsize(abs_path) dic = {'filename': filename, 'filesize': filesize} str_dic = json.dumps(dic) sk.send(str_dic.encode('utf-8')) with open(abs_path, 'rb') as f: content = f.read() sk.send(content) sk.close()
上面代码存在的问题:发送的字典可能和文件内容发送粘包;发送大文件时,接收的文件大小比原来的小
import socket import json # 接收 import struct sk = socket.socket() sk.bind(('127.0.0.1', 9002)) sk.listen() conn, addr = sk.accept() msg_len = conn.recv(4) dic_len = struct.unpack('i', msg_len)[0] msg = conn.recv(dic_len).decode('utf-8') msg = json.loads(msg) with open(msg['filename'], 'wb') as f: while msg['filesize'] > 0: content = conn.recv(1024) # msg['filesize'] -= 1024 传过来的大小不一定是1024 msg['filesize'] -= len(content) f.write(content) conn.close() sk.close()
import os import json import struct import socket # 发送 sk = socket.socket() sk.connect(('127.0.0.1', 9002)) # 文件名、文件大小 # 必须是一个绝对路径 abs_path = r'E:\A.PythonProject\action\day30\作业\a.txt' filename = os.path.basename(abs_path) filesize = os.path.getsize(abs_path) dic = {'filename': filename, 'filesize': filesize} str_dic = json.dumps(dic) b_dic = str_dic.encode('utf-8') mlen = struct.pack('i', len(b_dic)) sk.send(mlen) # 4个字节 表示字典转为字节之后的长度 sk.send(b_dic) # 具体的字典的数据 with open(abs_path, 'rb') as f: while filesize > 0: content = f.read(1024) filesize -= 1024 sk.send(content) sk.close()
import hashlib import os import socket secret_key = b'nice@123!' sk = socket.socket() sk.bind(('127.0.0.1', 9001)) sk.listen() conn, addr = sk.accept() # 创建一个随机的字符串 rand = os.urandom(32) # 发送随机字符串 conn.send(rand) # 根据发送的字符串 + secret_key 进行摘要 sha = hashlib.sha1(secret_key) sha.update(rand) res = sha.hexdigest() # 等待接收客户端的摘要结果 res_client = conn.recv(1024).decode('utf-8') # 做对比,如果一致,就显示是合法的客户端,并可以继续操作 if res_client == res: print('是合法的客户端') conn.send(b'hello') else: # 如果不一致,应关闭连接 conn.close()
import hashlib import socket secret_key = b'nice@123!' sk = socket.socket() sk.connect(('127.0.0.1', 9001)) # 接收服务端发送的随机字符串 rand = sk.recv(32) # 根据发送的字符串 + secret_key 进行摘要 sha = hashlib.sha1(secret_key) sha.update(rand) res = sha.hexdigest() # 摘要结果发送回服务端 sk.send(res.encode('utf-8')) # 继续和服务端进行通信 msg = sk.recv(1024) print(msg)
用来替代 hashlib 模块
import os import hmac h = hmac.new(b'nice@123!', os.urandom(32)) ret = h.digest() print(ret)
所以验证客户端合法性的代码可以简化。
socketserver 模块是基于 socket 模块完成的,socket 模块是底层模块,封装度低,效率不固定;socketserver 模块封装度高,效率比较固定。
tcp 协议的server端处理并发的客户端请求
import socketserver import time class Myserver(socketserver.BaseRequestHandler): # 重写handle def handle(self): # print(self.request) # <socket.socket fd=472, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9001), raddr=('127.0.0.1', 9040)> # 相当于conn,既有自己的ip,又有客户端的ip conn = self.request while 1: try: content = conn.recv(1024).decode('utf-8') conn.send(content.upper().encode('utf-8')) time.sleep(0.5) except ConnectionResetError: break server = socketserver.ThreadingTCPServer(('127.0.0.1', 9001), Myserver) server.serve_forever()
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9001)) while 1: sk.send(b'hello') content = sk.recv(1024).decode('utf-8') print(content)