网络编程
一. 软件开发的架构
1. C/S架构:
C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。
2. B/S架构
B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。
Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),
客户端Browser浏览器就能进行增删改查。
二. 网络基础
1 硬件:
网卡:在计算机中 帮助我们完成网络通信, 这个硬件出厂的时候就被分配了一个mac地址(计算机的唯一识别码)
交换机 :在局域网内多台机器之间通信
路由器 :多个局域网之间的机器之间的通信
2. 局域网 :一个区域内的多台机器组成的一个内部网络
3. 域名 : 和ip地址有一个对应关系,我们访问的域名经过解析也能得到一个ip地址
4. 协议类 :
arp协议 : 通过ip地址获取mac地址
ip协议 : ip地址的规范
ipv4\ipv6
5. 地址:
ipv4 由 4个点分十进制组成 0.0.0.0 ---255.255.255.255
三个被保留作专用网络的地址块(保留字段)
24位块: 10.0.0.0---10.255.255.255
16位块: 172.16.0.0---172.31.255.255
8位块: 192.168.0.0---192.168.255.255
本地的回环地址: 127.0.0.1自己这台机器能找到
全网段地址:0.0.0.0
子掩码:255.255.255.0
ipv6 由 8组每组包含4个十六进制字符(即0-F),而各组之间是以冒号分开
网关ip:在一台机器对局域网外的地址进行访问的时候使用的出口ip
6. 端口:
英文port的意译,可以认为是设备与外界通讯交流的出口,
帮助我们找机器上的对应服务 0-65535 惯用的端口号8000之后
三. osi协议
osi 七层协议
四. TCP协议和UDP协议
TCP协议:
可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。
使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
三次握手四次挥手
TCP协议server的四大步骤:sblac
1. socket()
2. bind()
3. listen()
4. accept()
5. close
#server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9001)) #把地址绑定到套接字
sk.listen() #监听链接
while True:
conn,addr = sk.accept() # #接受客户端链接
while True:
msg = input('>>>')
conn.send(msg.encode('utf-8')) #向客户端发送信息
if msg.upper() == 'Q': #q退出
break
content = conn.recv(1024).decode('utf-8') # 等待客户端消息
if content.upper() == 'Q': break #和客户端一起退出
print(content)
conn.close()
sk.close()
TCP协议client端的两大步骤:sc
1 socket()
2 connect()
#client 端
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
while True:
ret = sk.recv(1024).decode('utf-8')
if ret.upper() == 'Q':break
print(ret)
msg = input('>>>')
sk.send(msg.encode('utf-8'))
if msg.upper() == 'Q':
break
sk.close()
UDP协议
不可靠的、无连接的服务,传输效率高(发送前时延小),
一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。
使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
UDP协议server端的步骤
1. socket(type=socket.SOCK_DGRAM)
2. bind()
3. recvfrom()/sendto()
4. sendto()/recvfrom()
5. close()
#server 端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9001)) #绑定服务器套接字
while True: # 对话(可与多个client接收与发送)
msg,client_addr = udp_sk.recvfrom(1024)
print(msg.decode('utf-8'))
content = input('>>>')
udp_sk.sendto(content.encode('utf-8'),client_addr)
udp_sk.close() # 关闭服务器套接字
#client 端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1',9001)
while True:
content = input('>>>')
if content.upper() == 'Q':break
udp_sk.sendto(content.encode('utf-8'),server_addr)
msg = udp_sk.recv(1024).decode('utf-8')
if msg.upper() == 'Q':break
print(msg)
udp_sk.close()
五. 黏包现象
先来看两端代码:
# server端
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'world')
conn.close()
sk.close()
# client端
import time
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
time.sleep(0.1) #睡0.1秒
msg1 = sk.recv(1024)
msg2 = sk.recv(1024)
print(msg1) # -->b'helloworld'
print(msg2) # --> b''
sk.close()
可以发现两次发送的内容都被一个recv接收了 而第二个recv没有接收到任何内容,这种现象就黏包现象
黏包现象:前后发送的数据黏在一起了
黏包现象的成因:
1. 发送端粘 : 合包机制,发送端需要等缓冲区满才发送出去,造成粘包
2. 接收端粘 : 接收不及时,接收方不及时接收缓冲区的包,造成多个包接收
(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
黏包的解决方案:
发送端:
1. 发送的是字符串
2. 将字符串转换成字节码
3. 计数字节码的长度
4. 用struct模块中的pack方法将字节码的长度转化成4个字节
5. 发送4个字节码
6. 发送要发送的内容的字节码
接收端:
1. 先接收4个字节
2. 在把四个字节转换成数字
3. 接收这个长度的信息
#server 端 import socket import struct sk = socket.socket() sk.bind(('192.168.16.67',9001)) sk.listen() while 1: conn,addr = sk.accept() while 1: size = conn.recv(4) #先接收四个字节的 代表数据总的长度 size = struct.unpack('i',size)[0] msg = conn.recv(size).decode('utf-8') #按着数据的长度接收到byte类型的数据 if msg.upper()=='Q':break print(msg) content = input('>>>') byte_content = content.encode('utf-8') size = struct.pack('i', len(byte_content)) conn.send(size) conn.send(byte_content) if content.upper()=='Q':break conn.close() sk.close()
#clienet端 import socket import struct sk = socket.socket() addr = ('192.168.16.67',9001) sk.connect(addr) while 1: msg = input('>>>') byte_msg = msg.encode('utf-8') #计算byte数据的长度 size = struct.pack('i',len(byte_msg)) #使用struct模块将数据长度转换成4个字节的固定长度 sk.send(size) #发送长度 sk.send(byte_msg) #发送数据 if msg.upper() == 'Q': break size = sk.recv(4) size = struct.unpack('i', size)[0] content = sk.recv(size).decode('utf-8') if content.upper()=='Q':break print(content) sk.close()
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。 不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,
在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,
而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,
这意味着udp根本不会粘包,但是会丢数据,不可靠。
用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。
用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送) 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),
这是指在用send函数时,数据长度参数不受限制。
而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,
会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
六.并发的socketserver
调用socketserver模块实现并发
# server端 import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): conn = self.request while 1: conn.send('嘿嘿嘿'.encode('utf-8')) #可以不断的发送数据到多个client端 print(conn.recv(1024).decode('utf-8')) #可以不断的接收多个client端发来的数据 server = socketserver.ThreadingTCPServer(('192.168.16.67', 9001), Myserver) server.serve_forever()
client端没有改变:
import socket sk = socket.socket() sk.connect(('192.168.16.67',9001)) while 1: print(sk.recv(1024).decode('utf-8')) sk.send('1111'.encode('utf-8'))
也可以使用不阻塞的方法实现并发:
#server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.setblocking(False) # blocking阻塞 setblocking设置阻塞 False就是不阻塞
sk.listen()
conn_l = []
while True:
try:
conn,addr = sk.accept()
conn_l.append(conn) #将客户端的连接添加到列表
except BlockingIOError:
for conn in conn_l: #将每一个客户端都拿出来收发消息
try:
msg = conn.recv(1024).decode('utf-8')
print(msg)
conn.send(msg.upper().encode('utf-8'))
except BlockingIOError:
pass
client端不用改变
七 验证客户端的合法性
一台server端面向多台client端 采用相同的方法来验证 验证通过才能交互
采用相同的hash算法来验证: server端和client端都有一段相同的密钥
验证时server端发送给client端一段随机的字节
比较server端和client端的计算结果
server端
# server端
import os
import hashlib
import socket
secret_key = b'hello'
#os.urandom(32) 给每一客户端发送一个随机32位的字符串
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.listen()
conn,addr = sk.accept()
rand = os.urandom(32)
conn.send(rand)
sha = hashlib.sha1(secret_key)
sha.update(rand)
res = sha.hexdigest()
ret = conn.recv(1024).decode('utf-8')
if ret == res:
print('是合法的客户端')
#进行接下来的交互
else:
print('不是合法的客户端')
conn.close()
sk.close()
client端
import socket import hashlib secret_key = b'hello' sk = socket.socket() sk.connect(('127.0.0.1', 9001)) rand = sk.recv(32) sha = hashlib.sha1(secret_key) sha.update(rand) res = sha.hexdigest() sk.send(res.encode('utf-8')) sk.close()
使用hmac模块更加便捷的实现验证:
server端:
#server端 import os import hmac import socket secret_key = b'hello' #os.urandom(32) 给每一客户端发送一个随机的byte类型的字符串 sk = socket.socket() sk.bind(('127.0.0.1',9001)) sk.listen() conn,addr = sk.accept() rand = os.urandom(32) conn.send(rand) hmac = hmac.new(secret_key,rand) res = hmac.digest() #hmac得到的就是byte类型的字符串 ret = conn.recv(1024) if ret == res: print('是合法的客户端') else: print('不是合法的客户端') conn.close()
client端:
import hmac
import socket
secret_key = b'hello'
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
rand = sk.recv(32)
hmac = hmac.new(secret_key,rand)
res = hmac.digest()
sk.send(res)
sk.close()