网络编程
目标:编写C/S架构的软件
网络编程演变过程
-
单机架构:不需要联网,如超级玛丽、坦克大战等。
-
C(client)/S(server)架构:客户端直接和服务端交互,如QQ、大型网络游戏等。
-
B(browser)/S(server)架构:客户端嫁接在浏览器上,浏览器和服务器交互,如淘宝、京东等。
客户端:用户安装的软件。 服务端:统一管理数据库的主机中的软件叫做服务器,再后来服务端不只是管理数据外加处理业务逻辑。 服务端特性:固定IP且稳定运行,支持并发。
大白话OSI五层协议(应传网数物)
-
物理层:电信号
-
数据链路层:
- 以太坊协议:将电信号进行分组,每组(数据报/数据帧)由报头和数据组成。
- MAC地址/物理地址:每块网卡有唯一一个MAC地址,12位16进制数表示(前六位是厂商编号,后六位是流水线号),发送者地址和接收者地址就是MAC地址,可确定唯一计算机。
- 广播:同一局域网通信,会产生广播风暴。
-
网络层:
- IP地址:目前用IPv4由32位二进制表示,从0.0.0.0到255.255.255.255范围内。
- 子网掩码:判断两个IP是否处于同一网段。
- ARP协议:广播的方式发送数据包,获取目标主机的MAC地址。
- MAC地址学习:MAC地址和IP地址的映射表,第一次接收就会在IP/MAC映射表中添加一条数据
{'172.16.10.1':ddsadfgegsdgsdg}
-
传输层:
- 端口号:端口范围0-65535,0-1023为系统占用端口,找到独一无二的应用程序。
- 半连接池:限制连接的最大数。
- DOS攻击——拒绝服务攻击和DDoS攻击——分布式拒绝服务攻击。
- UDP协议和TCP协议(后面详细讲)
-
应用层:应用程序均工作于应用层,数据交互。
上网流程分析
- 在浏览器输入域名,隐藏端口。
- 通过dns服务器将域名解析成IP地址。
- 向IP+端口号这个地址发送请求,就会访问到域名所在的服务器。
TCP协议和UDP协议相关
tcp协议(流式协议):通信需要建立连接,通信结束要释放连接,不允许发空数据,所以是可靠传输,但增加确认、流量等开销,对应的应用层协议如HTTP、FTP等。
基于TCP协议的套接字编程:
import socket
server = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象,默认tcp协议
server.bind(('127.0.0.1', 8001)) # 绑定IP和端口号
server.listen(5) # 半连接池大小
while True: # 连接循环
conn, addr = server.accept()
while True: # 通信循环
try:
data = conn.recv(1024) # 接收客户端数据
print(data)
conn.send(data.upper()) # 发送数据
except Exception:
break
# 客户端.py
import socket
client = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象
client.connect(('127.0.0.1', 8001)) # 和客户端建立连接
while True: # 通信循环
msg = input('>>>:')
if not msg: continue # 发送数据不能为空
client.send(bytes(msg, encoding='utf-8')) # 以bytes格式发送数据
data = client.recv(1024) # 接受服务端数据
print(data)
udp协议(数据报协议):通信不需要建立连接和确认,允许发空数据,所以是不可靠传输,省去很多开销,对应的应用层协议如DNS、NFS等。
基于UDP协议的套接字编程:
# 服务端.py
import socket
server = socket.socket(type=socket.SOCK_DGRAM) # 建立socket对象
server.bind(('127.0.0.1', 8000)) # 绑定IP和端口号
while True: # 通信循环
data, addr = server.recvfrom(1024) # 接受客户端信息
print(data)
server.sendto(data.upper(), addr) # 发送服务端信息
# 客户端.py
import socket
client = socket.socket(type=socket.SOCK_DGRAM) # 建立socket对象
while True: # 通信循环
msg = input('>>>:')
client.sendto(bytes(msg, encoding='utf-8'), ('127.0.0.1', 8000)) # 以bytes格式发送数据
data = client.recvfrom(1024) # 接受服务端数据
print(data)
套接字(socket):在应用层和传输层之间的一个抽象层,把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信,即IP地址+端口号组成,但是套接字所写的软件均属于应用层。
模拟ssh远程执行命令
在客户端模拟ssh发送指令,服务端通过subprocess执行该命令,然后返回命令的结果。
# 服务端.py
import socket
import subprocess
server = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象,默认tcp协议
server.bind(('127.0.0.1', 8001)) # 绑定IP和端口号
server.listen(5) # 半连接池大小
while True: # 连接循环
conn, addr = server.accept()
while True: # 通信循环
try:
cmd = conn.recv(1024) # 接收客户端数据
print(cmd)
# 执行cmd命令,然后把执行结果保存到管道里
pipeline = subprocess.Popen(str(cmd, encoding='utf-8'), # 输入的字符串形式的cmd命令
shell=True, # 通过shell来运行
stdout=subprocess.PIPE, # 把正确输出放入管道,以便打印
stderr=subprocess.PIPE) # 把错误输出放入管道,以便打印
stdout = pipeline.stdout.read() # 打印正确输出
stderr = pipeline.stderr.read() # 打印错误输出
conn.send(stdout) # 发送数据
conn.send(stderr) # 发送数据
except Exception:
break
# 客户端.py
import socket
client = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象
client.connect(('127.0.0.1', 8001)) # 和客户端建立连接
while True: # 通信循环
cmd = input('>>>:')
if not cmd: continue # 发送数据不能为空
client.send(bytes(cmd, encoding='utf-8')) # 以bytes格式发送数据
data = client.recv(1024) # 接受服务端数据
print(str(data, encoding='gbk'))
#输入dir命令,由于服务端发送字节少于1024字节,客户端可以接受。
#输入tasklist命令,由于服务端发送字节多于1024字节,客户端只接受部分数据,并且当你再次输入dir命令的时候,客户端会接收dir命令的结果,但是会打印上一次的剩余未发送完的数据,这就是粘包问题。
粘包问题
形成粘包问题的原因:两个数据非常小且间隔时间短;数据太大,一次取不完,下一次还会取这个数据。
解决粘包问题的方案:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。
# 服务端.py
import socket
import subprocess
import json
import struct
server = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象,默认tcp协议
server.bind(('127.0.0.1', 8001)) # 绑定IP和端口号
server.listen(5) # 半连接池大小
while True: # 连接循环
conn, addr = server.accept()
while True: # 通信循环
cmd = conn.recv(1024) # 接收客户端数据
print(cmd)
# 执行cmd命令,然后把执行结果保存到管道里
pipeline = subprocess.Popen(str(cmd, encoding='utf-8'), # 输入的字符串形式的cmd命令
shell=True, # 通过shell来运行
stdout=subprocess.PIPE, # 把正确输出放入管道,以便打印
stderr=subprocess.PIPE) # 把错误输出放入管道,以便打印
stdout = pipeline.stdout.read() # 打印正确输出
stderr = pipeline.stderr.read() # 打印错误输出
# 制作报头
header_dic = {
'filename': 'a.txt',
'total_size': len(stdout) + len(stderr),
'hash': 'asdfpoi79032'
}
# 将报头序列化成字符串报头
header_json = json.dumps(header_dic)
# 将字符串报头转换成bytes格式
header_bytes = bytes(header_json, encoding='utf-8')
# 1.先将报头的长度len(header_bytes)打包成4个bytes,然后发送
conn.send(struct.pack('i', len(header_bytes)))
# 2.再发送报头
conn.send(header_bytes)
# 3.最后发送真实数据
conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()
# 客户端.py
import socket
import struct
import json
client = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象
client.connect(('127.0.0.1', 8001)) # 和客户端建立连接
while True: # 通信循环
cmd = input('>>>:')
if not cmd: continue # 发送数据不能为空
client.send(bytes(cmd, encoding='utf-8')) # 以bytes格式发送数据
# 1.先收4个字节,这4个字节包含报头的长度
header_len = struct.unpack('i', client.recv(4))[0]
# 2.再接收报头
header_bytes = client.recv(header_len)
# 3.从包中解析出想要的数据
header_json = str(header_bytes, encoding='utf-8')
header_dic = json.loads(header_json)
total_size = header_dic['total_size']
# 4.最后接收真实的数据
recv_size = 0
res = b''
while recv_size < total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(str(res, encoding='gbk'))
client.close()
基于socketserver实现并发的socket套接字编程
让服务端同时和多个客户端进行连接。
# 服务端.py
import socketserver
# 自己定义一个类,必须继承BaseRequestHandler
class MyTcp(socketserver.BaseRequestHandler):
# 必须重写handle方法
def handle(self):
try:
while True: # 通信循环
# 给客户端回消息
# conn对象就是request
data = self.request.recv(1024) # 接收数据
print(self)#<__main__.MyTcp object at 0x0000015941D1ECC0>
print(self.client_address)#('127.0.0.1', 11515)
print(data)
if len(data) == 0:
return
self.request.send(data.upper()) # 发送数据
except Exception:
pass
if __name__ == '__main__':
# 实例化得到一个tcp连接的对象,Threading是说,只要来了请求,自动的开线程来处理连接跟交互数据
# 第一个参数是绑定的地址,第二个参数传一个类
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8009), MyTcp)
# 一直在监听,只要来一个请求,就起一个线程做交互
server.serve_forever()
# 客户端.py
import socket
client = socket.socket(type=socket.SOCK_STREAM) # 建立socket对象
client.connect(('127.0.0.1', 8009)) # 和客户端建立连接
while True: # 通信循环
cmd = input('>>>:')
if not cmd: continue # 发送数据不能为空
client.send(bytes(cmd, encoding='utf-8')) # 以bytes格式发送数据
data = client.recv(1024) # 接受服务端数据
print(str(data, encoding='gbk'))
面试题
-
为什么客户端最后还要等待2MSL?
答:MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
- 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
- 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
-
为什么建立连接是三次握手,断开连接是四次挥手?
答:建立连接的时候,服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
-
如果已经建立了连接,但是客户端突然出现了故障怎么办?
答:TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
-
数据放在服务端和客户端的利与弊?
答:服务端统一处理有更好的安全性和稳定性而且升级比较容易,不过服务器负担就增加了。而客户端将负担分担到每个用户,从而节约服务器资源,安全性和稳定性可能会有一定的问题,但是升级比较麻烦。