一,客户端、服务器架构
1,硬件C\S架构(打印机)
2,软件C\S架构(web服务)
常用的软件服务器是web服务器,一台机器里放一些网页或web应用程序,然后启动服务,这样的服务器的任务就是接受客户的请求,把网页发给客户(如计算机上的浏览器),之后等待下一个用户请求。这些服务器启动的目标就是“永远的运行下去”。显然他们不可能实现这样的目标,但只要没有关机或硬件出错等外力干扰,他们就能运行非常长的一段时间。
生活中的C\S架构:
老男孩是S端,学生是C端
饭店是S端,吃饭的人是C端
C\S架构与socket的关系:
我们学习socket就是为了完成C\S架构的开发
二,osi七层
一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)
如果你要跟别人一起玩,那你就需要上网了(访问个黄色网站,发个黄色微博啥的),互联网的核心就是由一堆协议组成,协议就是标准,全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。人们按照分工不同把互联网协议从逻辑上划分了层级
1.首先:本节课程的目标就是教会你如何基于socket编程,来开发一款自己的C/S架构软件
2.其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的
3.然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。
4.最后:就让我们从这些标准开始研究,开启我们的socket编程之旅
TCP\IP协议包括运输层,网络层,链路层。
三,socket层
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
socket也是套接字
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
1,用socket模拟打电话:
服务器端:
1 import socket
2 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机
3 phone.bind(("127.0.0.1",8000)) #绑定手机卡
4 phone.listen(5) #开机 最多链接5个客户端
5
6 print("starting......")
7 while True: #链接循环
8 conn,addr=phone.accept() #等待电话链接
9 print("电话线路是",conn)
10 print("客户端手机号是",addr)
11
12 while True: #通讯循环
13 try:
14 data = conn.recv(1024) #收消息 最大接收1024字节
15 print("客户发来的消息是",data)
16
17 conn.send(data.upper()) #回客户端的消息
18 except Exception:
19 break
20
21 conn.close() #挂断电话
22 phone.close() #关机
客户端:
1 import socket
2 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
3 phone.connect(("127.0.0.1",8000))
4 while True:
5 msg = input(">>>>>>>>:").strip()
6 if not msg:continue
7 phone.send(msg.encode("utf-8")) #给服务端发的消息
8 data = phone.recv(1024) #接收服务端传回来的消息
9 print(data)
10
11
12 phone.close() #关闭手机
服务端套接字函数:
1 s.bind() #绑定(主机,端口号)到套接字
2 s.listen() #开始TCP监听
3 s.accept() #被动接受TCP客户的链接,(阻碍式)等待连接的到来
客户端套接字函数:
1 s.connect() #主动初始化TCP服务器连接
2 s.connect_ex() #函数的扩展版本,出错时返回出错码,而不是抛出异常
套接字家族的名字:AF_INET
AF_INET是使用最广泛的一个,我们网络编程基本用这个
四,基于TCP的套接字
1,模拟windons命令:
server端:
1 #基于tcp的套接字
2 import socket,subprocess
3 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机
4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
5 phone.bind(("127.0.0.1",8000)) #绑定手机卡
6 phone.listen(5) #开机 最多链接5个客户端
7
8 print("正在链接服务器.....")
9 while True: #链接循环
10 conn,addr=phone.accept() #等待电话链接
11 print("连接成功......")
12
13 while True: #通讯循环
14 try:
15 cmd = conn.recv(1024)#收消息 最大接收1024字节
16
17 msg = subprocess.Popen(cmd.decode("utf-8"),
18 shell=True,
19 stdout=subprocess.PIPE,
20 stderr=subprocess.PIPE)
21 new_res = msg.stdout.read()
22 res = msg.stderr.read()
23
24 if not res:
25 conn.send(new_res) #回客户端的消息
26 conn.send(res)
27 except Exception:
28 break
29
30 conn.close() #挂断电话
31 phone.close() #关机
client端:
1 import socket
2 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
3 phone.connect(("127.0.0.1",8000))
4 while True:
5 msg = input(">>>>>>>>:").strip()
6 if not msg:continue
7 phone.send(msg.encode("utf-8")) #给服务端发的消息
8 data = phone.recv(1024) #接收服务端传回来的消息
9 print(data.decode("gbk"))
10
11
12 phone.close() #关闭手机
五,基于UDP的套接字:
server端:
1 import socket
2
3 ip_port = ("127.0.0.1",8081)
4 server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
5 server.bind(ip_port)
6 bufsize = 1024
7
8 while True:
9 conn, client_addr = server.recvfrom(bufsize)
10 print("来自[%s]的一条消息:%s"%(client_addr,conn.decode("utf-8")))
11 inp = input("回复消息:")
12 if not inp:continue
13 server.sendto(inp.encode("utf-8"),client_addr)
client端:
import socket
ip_port = ("127.0.0.1",8081)
bufsize = 1024
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
inp = input("请输入发送内容:").strip()
if not inp:continue
client.sendto(inp.encode("utf-8"),ip_port)
data,server_addr = client.recvfrom(bufsize)
print("来自[%s]的一条消息:%s"%(server_addr,data.decode("utf-8")))
client.close()
六,recv与recvfrom的区别
发消息:都是将数据发送到已端的缓存中。
收消息:都是从已端的缓存中收。
----------- part1 -------------
1,tcp:send发消息,recv收消息
2,udp:sendto发消息,recvfrom收消息
----------- part2:send与sendto -------------
tcp 是基于数据流的,udp是基于数据报的,最后都是以bytes类型发送到对方
1,send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包
2,sendto(bytes_data,ip_port):发送数据报,bytes_data若为空,还有ip_port,所以即便是发现空的bytes_data,数据其实也不是空的,自己这端的缓冲区收到内容,操作系统就会控制udp协议发包
----------- part3:recv与revfrom -------------
1,tcp协议:
1.1 如果收消息缓冲区里的数据为空,那么recv就会阻塞(就是一直等着收)
1.2 只不过tcp协议的客服端send一个空数据就是真的空数据,客户端即使有无穷个send空,也跟没有是一样的
1.3 tcp基于链接通信
基于链接,则需要listen(backlog),指定连接池的大小
基于链接,必须先运行服务端,然后客户端发起链接请求
对于windows和linux系统:如果一端断开了链接,另一端的链接也会断开,recv不会阻塞,收到的是空(解决方法:服务器端通信循环内加异常处理,捕捉到异常后就break 跳出通信循环)
2,udp协议:
2.1 如果收消息缓冲区里的数据为空,recvfrom也会阻塞,udp协议的客服端sendto一个空数据并不是真的空数据(空数据+地址信息,得到的数据报仍然不会为空),所以客户端只要有一个sendto(不管是否发送空数据,都不是真的空数据),服务端就可以recvfrm到数据。
2.2 udp无连接
无连接,因而无需listen(backlog),就没有什么连接之说了
无连接,udp的sendto不管是否有一个正在运行的服务端,可以在客服端一直发消息,但是,发出去的消息服务端没有接受到,所以就丢失了
recvfrom收的数据小于sendto发送的数据时,在max和linux系统上数据直接丢失,在windows系统上发送的比接收的数据大,直接报错
只有sendto数据,而没有recvfrom收数据,结果就是,数据丢失。
注意:
单独运行udp客服端,没有事,因为udp协议只负责把包发出去,服务端收不收,客户端根本不管。而要是单独运行tcp客户端,会报错,因为tcp是基于链接的,必须有一个服务端先运行着,客户端去跟服务端建立链接,然后依托于链接才能传递消息,任何一方把链接关闭,都会导致对方的程序崩溃。
七,粘包
只有tcp有粘包现象,而udp永远不会发生粘包
因为tcp是面向数据流的,就如同一条水流,一条水流不知道在哪里开始,在哪里结束。比如客户端发送一个20字节的数据,服务端一次只接受1字节的数据,剩下的数据还会存在服务端的缓存里,这时,客户端又发来了一条20字节的数据,服务端这次改为接收1024字节(0-1024),结果,第一次剩下的数据跟这次的数据粘到一块了,一并接收了过来,这种情况就是 粘包。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取出多少字节的数据所造成的。
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对一个一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
两种情况下会发生粘包。
1,发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包),
2,接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
八,解决粘包的方法:
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到服务端,服务端在接收时,先从缓存中取出自定义的报头长度,再取报头,最后再取真是的数据
struct模块
该模块可以吧一个类型,如数字,转成固定长度的bytes
通常还有json模块
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
这里模拟windows的命令
服务端:
1 #server端,解决粘包问题
2 import socket
3 import struct
4 import json
5 import subprocess
6 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
7 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
8
9 phone.bind(('127.0.0.1',8080))
10
11 phone.listen(5)
12 #开机 等待电话链接
13 #listen()TCP的缓存链接池,可以将别的等待链接挂起,方便之后链接。测试的时候可以写5,但在实际应用中,应该在配置文件中设置这个值
14 while True:#链接循环
15 print('start and waiting')
16 conn,addr = phone.accept()
17 while True: #通讯循环
18 try:
19 data = conn.recv(1024)
20 # 收消息,1024:单次接收的量;设置的时候考虑到获取的消息来自自己的缓存池,所以不能很大,通常为1024或8192
21 # linux上客户强制断开后,服务端会接收空信息,空会使服务端进入无限接收发送的循环,所以需要判断是否为空,若为空就直接判断断开连接
22 if not data:break
23 print('>>>:',data.decode('utf-8'))
24 res = subprocess.Popen(str(data.decode('utf-8')), shell=True,
25 stdout=subprocess.PIPE,
26 stderr=subprocess.PIPE)
27 out_res = res.stdout.read()
28 err_res = res.stdout.read()
29 data_size = len(out_res) + len(err_res)
30 head_dic = {"data_size":data_size} #自定义报头 字典的格式
31 head_json = json.dumps(head_dic) # #序列化json字符串
32 head_bytes = head_json.encode("utf-8") # 编码成bytes
33
34 #part1:先发报头的长度
35 head_len = len(head_bytes)
36 conn.send(struct.pack("i",head_len)) #把报头的长度统一打包为4个字节,发出去
37
38 #part2:再发报头
39 conn.send(head_bytes)
40
41 #part3:最后发送数据部分
42 conn.send(out_res)
43 conn.send(err_res)
44
45
46 except Exception:
47 break
48 conn.close()
49
50 phone.close()
客户端:
1 #client端,解决粘包问题
2 import socket
3 import struct
4 import json
5 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
6 phone.connect(('127.0.0.1',8080))
7
8 while True:
9 cmd = input('>>:').strip()
10 if not cmd:continue
11 phone.send(cmd.encode('utf-8'))#编码成bytes 格式才能转发出去
12
13 #part1:先接收报头的长度
14 head_struct = phone.recv(4)
15 head_len = struct.unpack("i",head_struct)[0] #解开报头的长度,返回的是一个元组,取第一个
16
17 # part2:在接收报头
18 head_bytes = phone.recv(head_len) # 接收报头的长度(字节)
19 head_json = head_bytes.decode("utf-8") #解码成betys
20 head_dic = json.loads(head_json) #反序列化json
21 data_size = head_dic["data_size"] #取出报头长度
22
23 # part3:最后接收数据
24 recv_size = 0
25 recv_data = b''
26 while recv_size < data_size:
27 data = phone.recv(1024) # 单次获取上限设置为1024,如果没有1024长度的数据,也没有关系
28 recv_size += len(data) # recv_size用于累加计算收集的字符数据长度
29 recv_data += data # 字符串拼接
30
31 print(recv_data.decode('gbk'))#因为是windows系统解码要gbk
32
33
34
35 phone.close()
FTP上传下载文件:
只做了上传,下载反过来就行,多并发,同时可以最大5个客服端上传文件
服务端:
1 import socketserver
2 import struct
3 import json
4 import os
5 import sys
6 import time
7
8 class Server(socketserver.BaseRequestHandler):
9 max_packet_size = 1024
10 coding = "utf-8"
11 requset_queue_size = 5
12 server_dir = r"F:\\file_upload"
13
14 def handle(self):
15 print(self.request)
16 while True:
17 try:
18 #print("已成功连接")
19 head_struct = self.request.recv(4)
20 if not head_struct:break
21 head_len = struct.unpack("i",head_struct)[0]
22 head_json = self.request.recv(head_len).decode(self.coding)
23 head_dic = json.loads(head_json)
24
25 print(head_dic)
26 cmd = head_dic["cmd"]
27 if hasattr(self,cmd):
28 func = getattr(self,cmd)
29 func(head_dic)
30 except Exception:
31 break
32
33 def put(self,args):
34 file_path = os.path.normpath(os.path.join(self.server_dir,args["filename"]))
35 filesize = args["filesize"]
36 recv_size = 0
37 print("------->",file_path)
38 with open(file_path,"wb") as f:
39 while recv_size < filesize:
40 recv_data = self.request.recv(self.max_packet_size)
41 f.write(recv_data)
42 recv_size+=len(recv_data)
43 print("recvsize : %s filesize : %s"%(recv_size,filesize))
44
45
46 if __name__ == '__main__':
47 obj = socketserver.ThreadingTCPServer(("127.0.0.1",8080),Server)
48 obj.serve_forever()
客户端:
1 import socket
2 import struct
3 import json
4 import os
5
6
7
8 class Client:
9 address_family = socket.AF_INET
10
11 socket_type = socket.SOCK_STREAM
12
13 allow_reuse_address = False
14
15 max_packet_size = 8192
16
17 coding='utf-8'
18
19 request_queue_size = 5
20
21 def __init__(self, server_address, connect=True):
22 self.server_address=server_address
23 self.socket = socket.socket(self.address_family,
24 self.socket_type)
25 if connect:
26 try:
27 self.client_connect()
28 except:
29 self.client_close()
30 raise
31
32 def client_connect(self):
33 self.socket.connect(self.server_address)
34
35 def client_close(self):
36 self.socket.close()
37
38 def run(self):
39 while True:
40 inp=input(">>: ").strip()
41 if not inp:continue
42 l=inp.split()
43 cmd=l[0]
44 if hasattr(self,cmd):
45 func=getattr(self,cmd)
46 func(l)
47
48
49 def put(self,args):
50 cmd=args[0]
51 filename=args[1]
52 if not os.path.isfile(filename):
53 print('file:%s is not exists' %filename)
54 return
55 else:
56 filesize=os.path.getsize(filename)
57
58 head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
59 print(head_dic)
60 head_json=json.dumps(head_dic)
61 head_json_bytes=bytes(head_json,encoding=self.coding)
62
63 head_struct=struct.pack('i',len(head_json_bytes))
64 self.socket.send(head_struct)
65 self.socket.send(head_json_bytes)
66 send_size=0
67 with open(filename,'rb') as f:
68 for line in f:
69 self.socket.send(line)
70 send_size+=len(line)
71 print(send_size)
72 else:
73 print('upload successful')
74
75
76
77
78 client=Client(('127.0.0.1',8080))
79
80 client.run()