一、客户端/服务端架构:
1、硬件C/S架构(客户端/服务端)
二、osi七层模型
七层: 物理层 数据链路层 网络层 传输层 会话层 表示层 应用层
引子:计算机系统由硬件,操作系统,应用软件三部分组成
三、socket是什么
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中socket其实是一种门面模式,对于用户来说它就是一组简单的接口。
有人将socket说成ip+port,ip是用来标识网络中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,
而程序的pid是同一台机器上不同的进程或者是线程的标识。
四、套接字的工作流程
服务端:初始化socket、绑定地址端口(bind)、监听(listen)、调用accept阻塞、等待客户端链接。
客户端:初始化socket、连接服务器(connect)
import socketsocket.socket(socket_famliy, socket_type, protocal=0)socket_famliy 可以是AF_UNIX或者是 AF_INET。socket_type可以是 SOCK_STREAM 或 SOCK_DGRAM。protocal 一般不填,默认值是0获取tcp/ip套接字tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)获取udp/ip套接字udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
服务端套接字函数s.bind() 绑定(主机,端口号)到套接字s.listen() 开始TCP监听s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来客户端套接字函数s.connect() 主动初始化TCP服务器连接s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常公共用途的套接字函数s.recv() 接收TCP数据s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)s.recvfrom() 接收UDP数据s.sendto() 发送UDP数据s.getpeername() 连接到当前套接字的远端的地址s.getsockname() 当前套接字的地址s.getsockopt() 返回指定套接字的参数s.setsockopt() 设置指定套接字的参数s.close() 关闭套接字面向锁的套接字方法s.setblocking() 设置套接字的阻塞与非阻塞模式s.settimeout() 设置阻塞套接字操作的超时时间s.gettimeout() 得到阻塞套接字操作的超时时间面向文件的套接字的函数s.fileno() 套接字的文件描述符s.makefile() 创建一个与该套接字相关的文件
五、基于TCP的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
tcp服务端:
ss = socket() #创建服务器套接字ss.bind() #把地址绑定到套接字ss.listen() #监听链接inf_loop: #服务器无限循环 cs = ss.accept() #接受客户端链接 comm_loop: #通讯循环 cs.recv()/cs.send() #对话(接收与发送) cs.close() #关闭客户端套接字ss.close() #关闭服务器套接字(可选)
tcp客户端
cs = socket() # 创建客户套接字cs.connect() # 尝试连接服务器comm_loop: # 通讯循环 cs.send()/cs.recv() # 对话(发送/接收)cs.close() # 关闭客户套接字
加上链接循环和通信循环
#_*_coding:utf-8_*___author__ = 'mosson'import socketip_port=('127.0.0.1',8081)#电话卡BUFSIZE=1024s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机s.bind(ip_port) #手机插卡s.listen(5) #手机待机while True: #新增接收链接循环,可以不停的接电话 conn,addr=s.accept() #手机接电话 # print(conn) # print(addr) print('接到来自%s的电话' %addr[0]) while True: #新增通信循环,可以不断的通信,收发消息 msg=conn.recv(BUFSIZE) #听消息,听话 # if len(msg) == 0:break #如果不加,那么正在链接的客户端突然断开,recv便不再阻塞,死循环发生 print(msg,type(msg)) conn.send(msg.upper()) #发消息,说话 conn.close() #挂电话s.close() #手机关机
#_*_coding:utf-8_*___author__ = 'mosson'import socketip_port=('127.0.0.1',8081)BUFSIZE=1024s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect_ex(ip_port) #拨电话while True: #新增通信循环,客户端可以不断发收消息 msg=input('>>: ').strip() if len(msg) == 0:continue s.send(msg.encode('utf-8')) #发消息,说话(只能发送字节类型) feedback=s.recv(BUFSIZE) #收消息,听话 print(feedback.decode('utf-8'))s.close()
问题:
有的同学在重启服务端时可能会遇到
这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)
解决办法:
#加入一条socket配置,重用ip和端口phone=socket(AF_INET,SOCK_STREAM)phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8080))
六、基于UDP的套接字
UDP是无链接的,先启动哪一端都不会报错
UDP服务端:
ss = socket() #创建一个服务器的套接字ss.bind() #绑定服务器套接字inf_loop: #服务器无限循环 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)ss.close() # 关闭服务器套接字
UDP客户端:
cs = socket() # 创建客户套接字comm_loop: # 通讯循环 cs.sendto()/cs.recvfrom() # 对话(发送/接收)cs.close()
七:粘包现象
让我们给予tcp先制作一个远程执行命令的程序:(1:执行错误命令 2:执行ls 3:执行ifconfig)
action:::
res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)
的结果的编码是以当前所在的系统为准
八:什么事粘包
Tip:只有tcp才有粘包现象,UDP永远不会粘包。
发送端可以是一k一k的发送数据,而对应的接收端可以是两k两k的接收数据,当然也有可能一次提走3k或者是6k的数据。应用程序所看到的数据是一个整体,或者说说是一个流stream。
tcp协议是面向流的数据,UDP协议是面向消息的。
tcp套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的
所谓的粘包,主要是因为接收方不知道消息之间的界限,不知道一次性需要提取多少数据,造成的。
1、TCP(transport control protocal)是面向连接的面向流,提供高可靠性服务,socket是成对的。发送端为了将多个发往服务端的包,更有效发到对方,使用了优化,将多次间隔较小且数据量小的数据,合并成一个大的数据块,这样接收端就难于分辨,必须提供科学的拆包机制
2、UDP(user datagram protocal)是无链接的面向消息的,提供高效率服务,不会使用合并算法,由于UDP支持一对多的模式,接收端的skbuff采用了链式结构
3、tcp是基于数据流的,于是收发消息不能为空(为空需要特殊处理),UDP的手法消息可以为空
两种情况下回发生粘包:
发送端需要等缓冲区满,才发送出去,造成粘包(发送数据时间间隔短,数据量小,会合到一起,产生粘包)
接收方不及时接收缓冲区的包,造成多个接受包(客户端发送了一段数据,服务端只接收了一小部分,服务端下次再接收的时候还是从缓冲区拿上次遗留的数据产生粘包)
九:粘包的解决办法:
为字节流加上固定长度的报头,报头中包含字节流长度,然后send到对端,对端在接收时,先从缓存中取出定长的报头,然后在根据报头取相应的数据。
struct模块:
>>> struct("i", 111111111111111111111)
struct.error:"i" format requires -2147483648 <= number <= 2147483647 #这个是范围
struct模块用法讲解:
import json,struct#假设通过客户端上传1T:1073741824000的文件a.txt#为避免粘包,必须自定制报头header={ 'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值#为了该报头能传送,需要序列化并且转为byteshead_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度#客户端开始发送conn.send(head_len_bytes) #先发报头的长度,4个bytesconn.send(head_bytes) #再发报头的字节格式conn.sendall(文件内容) #然后发真实内容的字节格式#服务端开始接收head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式header=json.loads(json.dumps(header)) #提取报头#最后根据报头的内容提取真实的数据,比如real_data_len=s.recv(header['file_size'])s.recv(real_data_len)
import socket,struct,jsonimport subprocessphone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8080))phone.listen(5)while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度 conn.sendall(back_msg) #在发真实的内容 conn.close()服务端(自定制报头)
#_*_coding:utf-8_*_import socket,time,structs=socket.socket(socket.AF_INET,socket.SOCK_STREAM)res=s.connect_ex(('127.0.0.1',8080))while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) l=s.recv(4) x=struct.unpack('i',l)[0] print(type(x),x) # print(struct.unpack('I',l)) r_s=0 data=b'' while r_s < x: r_d=s.recv(1024) data+=r_d r_s+=len(r_d) # print(data.decode('utf-8')) print(data.decode('gbk')) #windows默认gbk编码客户端(自定制报头)
FTP的上传下载:
import socketimport structimport jsonimport subprocessimport osclass MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 server_dir='file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn,self.client_addr=self.get_request() print('from client ',self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct:break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) except Exception: break def put(self,args): file_path=os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize=args['filesize'] recv_size=0 print('----->',file_path) with open(file_path,'wb') as f: while recv_size < filesize: recv_data=self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size+=len(recv_data) print('recvsize:%s filesize:%s' %(recv_size,filesize))tcpserver1=MYTCPServer(('127.0.0.1',8080))tcpserver1.run()#下列代码与本题无关class MYUDPServer: """UDP server class.""" address_family = socket.AF_INET socket_type = socket.SOCK_DGRAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' def get_request(self): data, client_addr = self.socket.recvfrom(self.max_packet_size) return (data, self.socket), client_addr def server_activate(self): # No need to call listen() for UDP. pass def shutdown_request(self, request): # No need to shutdown anything. self.close_request(request) def close_request(self, request): # No need to close anything. pass
import socketimport structimport jsonimport osclass MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp=input(">>: ").strip() if not inp:continue l=inp.split() cmd=l[0] if hasattr(self,cmd): func=getattr(self,cmd) func(l) def put(self,args): cmd=args[0] filename=args[1] if not os.path.isfile(filename): print('file:%s is not exists' %filename) return else: filesize=os.path.getsize(filename) head_dic={ 'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} print(head_dic) head_json=json.dumps(head_dic) head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size=0 with open(filename,'rb') as f: for line in f: self.socket.send(line) send_size+=len(line) print(send_size) else: print('upload successful')client=MYTCPClient(('127.0.0.1',8080))client.run()
十:认证客户端的链接的合法性
如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不想ssl那么复杂, 那么利用hmac+盐的方式来实现
#_*_coding:utf-8_*_from socket import *import hmac,ossecret_key=b'linhaifeng bang bang bang'def conn_auth(conn): ''' 认证客户端链接 :param conn: :return: ''' print('开始验证新链接的合法性') msg=os.urandom(32) conn.sendall(msg) h=hmac.new(secret_key,msg) digest=h.digest() respone=conn.recv(len(digest)) return hmac.compare_digest(respone,digest)def data_handler(conn,bufsize=1024): if not conn_auth(conn): print('该链接不合法,关闭') conn.close() return print('链接合法,开始通信') while True: data=conn.recv(bufsize) if not data:break conn.sendall(data.upper())def server_handler(ip_port,bufsize,backlog=5): ''' 只处理链接 :param ip_port: :return: ''' tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(backlog) while True: conn,addr=tcp_socket_server.accept() print('新连接[%s:%s]' %(addr[0],addr[1])) data_handler(conn,bufsize)if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 server_handler(ip_port,bufsize)服务端
#_*_coding:utf-8_*_from socket import *import hmac,ossecret_key=b'linhaifeng bang bang bang'def conn_auth(conn): ''' 验证客户端到服务器的链接 :param conn: :return: ''' msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest)def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() if not data:continue if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close()if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)客户端(合法)
#_*_coding:utf-8_*_from socket import *def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) while True: data=input('>>: ').strip() if not data:continue if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close()if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)客户端(非法:不知道加密方式)
#_*_coding:utf-8_*_from socket import *import hmac,ossecret_key=b'linhaifeng bang bang bang1111'def conn_auth(conn): ''' 验证客户端到服务器的链接 :param conn: :return: ''' msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest)def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() if not data:continue if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close()if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)客户端(非法:不知道secret_key)
十一:socketserver实现并发。
基于tcp的套接字,关键就是两个循环,一个是链接循环(server),一个是通信循环(request)
server类:
request类:
继承关系:
以下述代码为例,分析socketserver源码:
ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer) ftpserver.serve_forever()
查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer
- 实例化得到ftpserver,先找类ThreadingTCPServer的__init__,在TCPServer中找到,进而执行server_bind,server_active
- 找ftpserver下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
- 执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
- 在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
- 上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....
源码分析总结:
基于tcp的socketserver我们自己定义的类中的
- self.server即套接字对象
- self.request即一个链接
- self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的
- self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
- self.client_address即客户端地址