本文共 20234 字,大约阅读时间需要 67 分钟。
客户端/服务器架构
C/S(Client/Server)架构 B/S(Browser/Server)架构OSI(Open System Interconnect)七层模型
Socket ●socket (套接字)起源于Unix , socket即是一 种特殊的文件,一些socket函数就是对其 进行的操作(读/写IO、打开、关闭) ●它是TCP/IP网络环境下应用程序与底层通信驱动程序之间运行的开发接口,它可以将应 用程序与具体的TCP/IP隔离开来,使得应用程序不需要了解TCP/IP的具体细节, 就能够 实现数据传输。 ●网络应用程序中, Socket通信是基于客户端/服务器结构。服务端
"""服务器监听的方式1.监听IP+端口2.监听套接字 -> socket监听文件,Server端-> socket 发送接收数据,client端 -> socket发送接收数据Server -> socket <- Client本地服务服务端:1.创建socket2.绑定ip+port <- 客户端来连 接的时候3.监听客户端的连按4.接收客户端的请求-> 阻塞状态客户端与服务端进行交互=>接收数据/发送数据5.关闭socket客户端:1.创建socket2.诖接服务端=> ip+port客户端与服务端进行交互=>接收数据/发送数据3.关闭socket"""import socket#创建一个套接字server = socket.socket()#绑定监听ip和端口#0.0.0.0表示监听本机所有ip地址server.bind(("0.0.0.0",8888))#监听,设置可以有多个客户端连接进来server.listen(2)#接收客户端,如果进行accept时,程序进入阻塞状态#返回两个数据,一个是客户端连接,一个是ip和端口print("start.....")conn,addr = server.accept()print("有一个客户端连接进来了",conn,addr)#接收客户端数据#python3在网络上进行传输的格式都是bytes格式的accept_data = conn.recv(1024)print("接收的数据为:",str(accept_data,encoding="utf8"))#给客户端发送一个数据conn.sendall(bytes("thankyou!",encoding="utf8"))conn.close()print("end......")"""如果服务端建立连接之后,第一件事情是recv那么客户端第一件事情一定要send所以在编写cs服务时,注意数据发送和接收一定要一一对应"""
客户端
import socketclient = socket.socket()#参数是一个元组,连接器client.connect(("127.0.0.1",8888))send_data = input(">>>")client.sendall(bytes(send_data,encoding="utf8"))accept_data = client.recv(1024)print(str(accept_data,encoding="utf8"))client.close()
优化-客户端和服务端进行多次数据交互
#服务端import socket#创建一个套接字server = socket.socket()#绑定监听ip和端口#0.0.0.0表示监听本机所有ip地址server.bind(("0.0.0.0",8888))#监听,设置可以有多个客户端连接进来server.listen(2)#接收客户端,如果进行accept时,程序进入阻塞状态#返回两个数据,一个是客户端连接,一个是ip和端口print("start.....")conn,addr = server.accept()print("有一个客户端连接进来了",conn,addr)while True: #接收客户端数据 #python3在网络上进行传输的格式都是bytes格式的 accept_data = conn.recv(1024) print("接收的数据为:",str(accept_data,encoding="utf8")) if not accept_data or accept_data == b"bye": print("客户端要求断开") break #给客户端发送一个数据 conn.sendall(bytes("thankyou!",encoding="utf8"))conn.close()server.close()print("end......")#客户端import socketclient = socket.socket()#参数是一个元组,连接器client.connect(("127.0.0.1",8888))while True: send_data = input(">>>") if not send_data.strip(): continue if send_data == "bye": break client.sendall(bytes(send_data,encoding="utf8")) accept_data = client.recv(1024) print(str(accept_data,encoding="utf8"))client.close()
优化-连接多个客户端
#服务端import socket#创建一个套接字server = socket.socket()#绑定监听ip和端口#0.0.0.0表示监听本机所有ip地址server.bind(("0.0.0.0",8888))#监听,设置可以有多个客户端连接进来server.listen(2)#接收客户端,如果进行accept时,程序进入阻塞状态#返回两个数据,一个是客户端连接,一个是ip和端口print("start.....")while True: conn,addr = server.accept() print("有一个客户端连接进来了",conn,addr) while True: #接收客户端数据 #python3在网络上进行传输的格式都是bytes格式的 accept_data = conn.recv(1024) print("接收的数据为:",str(accept_data,encoding="utf8")) if not accept_data or accept_data == b"bye": print("客户端要求断开") break #给客户端发送一个数据 conn.sendall(bytes("thankyou!",encoding="utf8")) conn.close()server.close()print("end......")#客户端import socketclient = socket.socket()#参数是一个元组,连接器client.connect(("127.0.0.1",8888))while True: send_data = input(">>>") if not send_data.strip(): continue if send_data == "bye": break client.sendall(bytes(send_data,encoding="utf8")) accept_data = client.recv(1024) print(str(accept_data,encoding="utf8"))client.close()
●socket ( )
s = socket.socket(socket_ family,socket type,protocal=0) socket family: socket.AF INET IPv4 (默认) eP) sockct.AF_ INET6 IPv6 socket.AF_ UNIX 只能够用于单一-的Unix系统进程间通信 socket_ type: socket.SOCK_ STREAM 流式socket , tor TCP (默认) socket.SOCK_ DGRAM 数据报式socket,forUDP socket.SOCK_ RAW原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_ RAW可以;其次,SOCK_ RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_ _HDRINCL套 接字选项由用户构造IP头。 config.pyaddr = ("0.0.0.0",8888)
server.py
import socketimport configwith socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as server: server.bind(config.addr) print("start") while True: data = server.recv(1024) print(data)
client.py
import socketimport configwith socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as client: while True: data =bytes(input(">>>"),encoding="utf8") if data == "exit": break client.sendto(data,config.addr)
TCP与UDP的区别
TCP =>面向连按,有状态的。确保数据完螯性 UDP =>面向无洼按。有可能会丢包 TCP服务=>需要先启动服务端,再劣客户端连接 ConnectionRefusedError: [Errno 111] Connection refused ConnectionRefusedError: [WinError 10061]由于日标计算札积极拒绝,无法连接。 UDP => 可以直接启动客户端。不报错,但是肯定也是无法发送数据给服务端的。 可能的原囚: 1.网络连通性=> 192.168.0.110 => ping 2.端口连通性=> telnet |通」, 马上会断开连接=>服务内部白名单 3.端不通? a. selinux => Disable b. firewalld/iptables c.服务是否启动 d.监听地端几是不是正确
●只有基于tcp的socket
●粘包:发送方发送两个字符串”hello" +”world" ,接收方却一次性接收到 了”helloworld" ●为什么会产生粘包? 应用程序所看到的数据是一个流( stream) , 一条消息有多少字节对应用程序是不可见 的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。 根本原因:两端互相不知道对方发送数据的长度server.py
import socketimport configwith socket .socket() as server: server.bind(config . addr) server.listen(2) print("start......") conn, addr = server. accept( ) print("有一个客户端连接进米了",conn, addr) accept_data = conn. recv(1024) print("接收到的数据为:",str(accept_data, encoding="utf-8")) accept_data = conn . recv(1024) print("接收到的数据为:", str(accept_data, encoding="utf-8")) print("end......")
client.py
import socketimport configwith socket.socket() as client: client.connect(config. addr) client.sendall(bytes("hello",encoding="utf-8")) client.sendall(bytes("world",encoding="utf-8"))
解决
"""TCP粘包的产生TCP是数据是Stream,一条消息有多少个字节,程序不可知的发送的时候,两条消息间隔很近接收的时候,不知道消息的长度,于是就两条消息当作一条消息按收了。sendall + input + sendall=>不粘包sendall + print + sendll =〉粘包如何解决?1.sendall + time.sleep + sendLL =>不粘包2.send + recv + send => """#方式一import socketimport configwith socket.socket() as client: client.connect(config. addr) a = input(">>>") client.sendall(bytes(a,encoding="utf-8")) b = input(">>>") client.sendall(bytes(b,encoding="utf-8"))#方式二#client.pyimport socketimport configwith socket.socket() as client: client.connect(config. addr) client.sendall(bytes("hello",encoding="utf-8")) client.recv(1024) client.sendall(bytes("world",encoding="utf-8"))#server.pyimport socketimport configwith socket .socket() as server: server.bind(config.addr) server.listen(2) print("start......") conn, addr = server.accept() print("有一个客户端连接进米了",conn, addr) accept_data = conn.recv(1024) print("接收到的数据为:",str(accept_data, encoding="utf-8")) conn.sendall(b"ok") accept_data = conn.recv(1024) print("接收到的数据为:", str(accept_data, encoding="utf-8")) print("end......")
"""1.启动ftp服务器2.连接ftp服务器3.client => filename_path /tmp/a.jpg 服务端读本地文件内容,发送给客户端 客户端保存服务器发送过来的内容4.在本地文件能打开能为一个客户端提供多次服务即可""""""服务端"""import osimport socketimport configwith socket.socket() as server: server.bind(config.ftp_addr) server.listen(2) while True: conn , addr = server.accept() print("客户端连接过来了~",addr,conn) #为客户端提供服务 while True: file_path = conn.recv(1024) if not file_path:break if not os.path.exists(file_path): conn.sendall("文件目录不存在".encode("utf8")) continue print("开始传输",file_path) with open(file_path,"rb") as f: file_data = f.read() conn.sendall(str(len(file_data)).encode("utf8")) conn.recv(1024) conn.sendall(file_data)"""ftp客户端"""import osimport socketimport configwith socket.socket() as client: client.connect(config.ftp_addr) #用户可以多次下载文件 while True: data = input("请输入您要下载的文件路径---exit退出") if not data.strip():continue if data == "exit": break filename = os.path.split(data)[-1] #将要下载的文件路径发送到服务器 client.send(bytes(data,encoding="utf-8")) #接收服务器发送过来的数据(思考:如果接收的文件数据特别大怎么办?服务端发送数据时,告诉我们文件大小是多少) file_length = client.recv(1024) client.sendall(b"ok") file_data = b"" while True: file_data += client.recv(1024) if len(file_data) >= int(file_length):break with open(filename,"wb") as f: f.write(file_data)
"""服务端"""import osimport socketimport threadingimport configdef worker(conn,addr): print("客户端连接过来了~",addr,conn) #为客户端提供服务 while True: file_path = conn.recv(1024) if not file_path:break if not os.path.exists(file_path): conn.sendall("文件目录不存在".encode("utf8")) continue print("开始传输",file_path) with open(file_path,"rb") as f: file_data = f.read() conn.sendall(str(len(file_data)).encode("utf8")) conn.recv(1024) conn.sendall(file_data)with socket.socket() as server: server.bind(config.ftp_addr) server.listen(2) while True: conn , addr = server.accept() t = threading.Thread(target=worker,args=(conn,addr)) t.start()"""ftp客户端"""import osimport socketimport configwith socket.socket() as client: client.connect(config.ftp_addr) #用户可以多次下载文件 while True: data = input("请输入您要下载的文件路径---exit退出") if not data.strip():continue if data == "exit": break filename = os.path.split(data)[-1] #将要下载的文件路径发送到服务器 client.send(bytes(data,encoding="utf-8")) #接收服务器发送过来的数据(思考:如果接收的文件数据特别大怎么办?服务端发送数据时,告诉我们文件大小是多少) file_length = client.recv(1024) client.sendall(b"ok") file_data = b"" while True: file_data += client.recv(1024) if len(file_data) >= int(file_length):break with open(filename,"wb") as f: f.write(file_data)
阻塞io
买一件事情的时候,等待回复/资源,如果没有得到回复资源,只会等待,其他任何事情都不做 买票=>老王去火车站买票=>排队->一直等->轮到他 烧水=>烧一壶谁=>等水开(在水开之前,什么都不做)非阻塞io
做一件事的时候,等待回复,先去干一件其他的事情,等一下再来问情况 买票 => 老王去火车站买票->排队看一下->隔N个小时再来看一下 烧水=>烧一壶谁=>等5分钟去看一眼是不是开了i0多路复用
nginx的并发,如果一个请求i/o,开启一个进程/线程处理这个请求 如果一下就进来2万个请求,是否直接开启2w个线程/进程处理请求 i/o多路复用模型=> 一个线程通过记录i/o状态来同时管理多个i/o,提升服务器的吞吐能力 一个线程管理1024个连接i/o,2万个20个线程 select,poll,epoll信号驱动io
买一件事情的时候,等待回复/资源,如果没有得到回复资源,先去干别的,第有资源,会通知 买票->留个信息给管理员->有票->打电话通知->去火车站交钱买票异步io
买一件事情的时候,等待回复/资源,如果没有得到回复资源,先去干别的,第有资源,自动调用并完成接下来的任务 买票->留个信息给管理员->有票->打电话通知->把票寄给你Select和poll的区别(select有数量的限制1024,poll没有数量的限制)
select-> 1024个fd ->对所有的描述符进行遍历->找出就绪转态的fd 换尿布=>select=>1024=>定期去检查每个baby是否尿片是不是湿了=>找到所有尿布湿了的小孩 找人=>select=>问每一个人epoll->等通知型
换尿布=>select=>1024=>高级尿布,感应器=>如果湿了,响…=>去给他换select/poll:
1、当有io就绪,select会遍历所有的fd,来找到就绪fd 2、每次调用select(), 都会把所有的fd_ set从用户态拷贝到内核态 3、select所监控的fd_ set有限制,一般来说最多 只能监听1024个。 4、poll跟select实现类似,只是poll 没有fd监控数量限制epoll:
1、没有并发连接的限制 2、效率提升不是采用轮询方式,而是回调(有结果通种) 3、fd在整个过程中只拷贝-次Python中的select模块专注于I/O多路复用,提供了select、poll、
epoll三个方法 poll epollI在linux中可用 , windows仅支持select。进程指定内核监听哪些文件描述符(最多监听1024个fd)的哪些事件,当没有文件描述符事件发生
时,进程被阻塞; 当一个或者多个文件描述符事件发生时1进程被唤醒。 文件描述符: 对于Linux而言,所有对设备或文件的操作都是通过文件描述符进行的。select方法用来监视文件描述符(当文件描述符条件不满足时, select会阻塞) ,当某个文件描述符状态改变
后,会返回三个列表。 fd_ r_ list, fd_ w_ list, fd_ e_ list = select.select(rlist, wlist, xlist, [timeout]) 参数:可接受四个参数(前三个必须) rlist: wait until ready for reading (所有的输入的data,就是指外部发过来的数据) 当序列中的fd满足“可读”条件时,则获取发生变化的fd并添加到fd r_ list中 wlist: wait until ready for writing ( 监控和接收所有要发出去的data ) ●当参数2序列中含有fd时,则将该序列中所有的fd添加到fd_ w_ list中 xlist: wait for an “exceptional condition” ( 监控错误信息) ●当参数3序列中的fd发生错误时,则将该发生错误的fd添加到fd_ e list中 三创教育 timeout:超时时间。当超时时间为空 ,则select会-直阻塞,直到监听的句柄发生变化小案例
"""select-server"""import socketimport selectimport configwith socket.socket() as server: server.bind(config.addr) server.listen(5) rlist = [server] while True: #当我们执行到这来,程序就会阻塞,等待rlist就绪 rl , _ , _ = select.select(rlist,[],[]) #遍历所有操作fd for fd in rl: #如果fd是server=>有新的连接过来 if fd == server: conn,addr = server.accept() print("有客户端连接过来了") #将客户端加入到监听列表 rlist.append(conn) #接收客户端发送的数据,并直接返回 msg = conn.recv(1024) conn.sendall(msg) else: #已经建立连接的客户端 msg = fd.recv(1024) if not msg: rlist.remove(fd) fd.close() else: fd.sendall(msg)"""select-client"""import socketimport configwith socket.socket() as client: client.connect(config.addr) while True: data = input("请输入要发送的数据(exit):") if not data.strip():continue elif data.strip() == "exit":break client.sendall(bytes(data,encoding="utf8")) print("recv:",client.recv(1024))
解决bug
"""select-server"""import socketimport selectimport configimport threadingdef newclient(conn): # 接收客户端发送的数据,并直接返回 msg = conn.recv(1024) conn.sendall(msg)def oldclient(fd): # 已经建立连接的客户端 msg = fd.recv(1024) if not msg: rlist.remove(fd) fd.close() else: fd.sendall(msg)with socket.socket() as server: server.bind(config.addr) server.listen(5) rlist = [server] while True: #当我们执行到这来,程序就会阻塞,等待rlist就绪 rl , _ , _ = select.select(rlist,[],[]) #遍历所有操作fd for fd in rl: #如果fd是server=>有新的连接过来 if fd == server: conn,addr = server.accept() print("有客户端连接过来了") #将客户端加入到监听列表 rlist.append(conn) t1 = threading.Thread(target=newclient,args=(conn,)) t1.start() else: t2 = threading.Thread(target=oldclient,args=(fd,)) t2.start()
select与poll
1、select采用轮询的方式遍历所有fd ,监控对应事件是否发生 2、select需要在用户空间与内核空间频繁拷贝fd 3、 select最大仅仅支持1024个文件描述符 4、poll与select相差不大, poll无文件描述符监听限制。epoll
1、注册新事件时,就将fd拷贝进内核,每个fd在整个过程中只会拷贝一次。 2、为每个fd指定一个回调函数 ,当设备就绪,调用这个回调函数。 3、epoll对文件描述符没有额外限制select.epoll()创建epoll对象
epoll.close()关闭epoll对象的文件描述符 epoll.closed检测epol对象是否关闭 epoll.fileno()返回epoll对象的文件描述符 epoll.register(fd[, eventmask])向epoll对象中注册fd和对应的事件 epoll.unregister(fd)取消注册 事件: EPOLLIN Available for read可读状态符为1 EPOLLOUT Available for write可写状态符为4 EPOLLERR Error condition happened on the assoc. fd发生错误状态符为8"""epoll-server"""import socketimport selectimport configwith socket.socket() as server: server.bind(config.addr) server.listen(5) #创建一个epoll对象 epoll_obj = select.epoll() # 将server注册带epoll_obj,监听读事件 epoll_obj.register(server,select.EPOLLIN) # 存放所有的客户端信息 connections = { } while True: # events 就绪的对象 events = epoll_obj.poll() for fd,event in events: print(fd,event) #区分新连接和已经建立的连接 #新连接 if fd == server.fileno(): conn,addr = server.accept() connections[conn.fileno()] = conn epoll_obj.register(conn,select.EPOLLIN) msg = conn.recv(1024) conn.sendall(msg) #已经建立的连接 else: conn = connections[fd] msg = conn.recv(1024) # 客户端断开连接,从epoll中取消注册,并断开连接 if not msg: epoll_obj.unregister(fd) conn.close() del connections[fd] else: conn.sendall(msg)"""epoll-client"""import socketimport configwith socket.socket() as client: client.connect(config.addr) while True: data = input("请输入要发送的数据(exit):") if not data.strip():continue elif data.strip() == "exit":break client.sendall(bytes(data,encoding="utf8")) print("recv:",client.recv(1024))
SocketServer内部使用I0多路复用以及“多线程” 和“多进程”, 从而实现并发处理多个客户端请求的Socket服务端。
即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者"进程”专门负责处理当前客户端的所有请求。 socketserver将socket模块和select模块进行了封装,简化网络服务器版的开发。 socketserver最主要的作用:就是实现一个并发处理。四个比较主要的类,其中常用的是TCPServer和UDPServer
1、 TCPServer 2、UDPServer 3、UnixStreamServer,类似于TCPServer提供面向数据流的套接字连接,但是旨在UNIX平台上可用; 4、UnixDatagramServer,类似于UDPServer提供面向数据报的套接字连接,但是旨在UNIX平台上可用; 两种支持异步处理的类: 1、ForkingMixIn ,为每一个客户端请求派生一个新的进程去专门处理; 2、ThreadingMixIn ,为每一个客户端请求派生一 个新的线程去专门处理; 继承自这两个类型的服务端在处理新的客户端连接时不会阻塞,而是创建新的进/线程专门处理安户端的请求.1、创建一一个请求处理类)继承BaseRequestHandlerclass类并且重写父类的handle()方
法,该方法将处理传入的请求。 2、必须实例化-个上面类型中的一个类(如TCPServer )传递服务器的地址和你上面创建 的请求处理类给这个TCPServer。 3、调用handle_ request()或者serve_ forever()方法来处理一个或多个请求 4、调用server close()关闭socket# socketserver-serverimport socketserverimport configclass MyRequestHandler(socketserver.BaseRequestHandler): """'这个类的功能-> 每一个请求过来, 应该如何处理""" def handle(self): """核心部分:处理接收的数据""" # self.request =>类定义的方法=>表示客户端的请求 while True: data = self.request.recv(1024) print(data) if not data: break # 发送数据回去 self.request.sendall(data)if __name__ == "__main__": # 一次只能处理一个客户端的请求! !因为这里并没有使用多进程或多线程 # server = socketserver . TCPServer(config. addr, MyRequestHandLer) # 一次处理qqr个客户端的请求! !因为这里并没有使用多进程或多线程 # server = socketserver.TCPServer(config.addr,MyRewuestHandler) server = socketserver.ThreadingTCPServer(config.addr,MyRequestHandler) print("start...") server.serve_forever() server.server_close()# socketserver-clientimport socketimport configwith socket.socket() as client: client.connect(config.addr) while True: data = input(">>>") if not data:continue if data == "exit":break client.sendall(bytes(data,encoding="utf8")) recv = client.recv(1024) print(str(recv,encoding="utf8")) client.close()
案例-聊天
"""服务器=>启动=>1.启动服务并监听2.服务端接收到用户连接3.接收用户数据=> @setnome =》返回欢迎您, XXX=》clientf 与client2通信=>保存起来=> {username: username-socket} =>实例属性/类局烟=》@username =>发送消息给username => 难点?=》username => username-socket =》socket.send(消息内容)=> 如果username找不到对应的socket, 给自己回一个消息-> (当前用户不存在)=如果断开连接,那么从字典中删除"""import socketserverimport configclass MyRequestHandler(socketserver.BaseRequestHandler): """这个类的功能 -> 每一个请求过来,应该如何处理""" socket_dict = dict() def handle(self): """核心部分:处理接收的数据""" print("一个新客户端连接来啦!") while True: print("当前所有的用户有", self.socket_dict) # self.request => 类定义的方法 => 表示客户端的请求 data = str(self.request.recv(1024), encoding="utf-8") if not data: break print("接收到的数据是:", data) if data.startswith("@setname"): print("设置用户名") # "@setname lxl abc" username = ''.join(data.split()[1:]) self.request.sendall(bytes(f"欢迎你!{username}!", encoding="utf-8")) # 设置当前用户名 => 动态为请求添加了一个socket self.username = username self.add_client(username, self.request) elif data.startswith("@"): # "@lihu xxxx xxx2" to = data.split()[0][1:] message = " ".join(data.split()[1:]) if to in self.socket_dict: # {"ryc": socket, "lxl": socket} print(f"给{to}发消息") self.socket_dict[to].sendall(bytes(message, encoding="utf-8")) self.request.sendall(bytes("", encoding="utf-8")) else: self.request.sendall(bytes(f"您聊天的对象({to})不存在", encoding="utf-8")) else: self.request.sendall(bytes(f"数据有误!{data}", encoding="utf-8")) @classmethod def add_client(cls, username, client): cls.socket_dict[username] = client print("当前所有的用户有",cls.socket_dict) @classmethod def remove_client(cls, username): cls.socket_dict.pop(username) def finish(self): """一个客户端断开""" if hasattr(self, "username"): self.remove_client(self.username) print(f"{self.username}退出了~")if __name__ == "__main__": # 一次只能处理一个客户端的请求!!因为这里并没有使用多进程或多线程 # server = socketserver.TCPServer(config.addr, MyRequestHandler) # 同时处理多个客户端的请求! server = socketserver.ThreadingTCPServer(config.addr, MyRequestHandler) print("start....") server.serve_forever() server.server_close()"""客户端=>启动1.连接服务器2.输入你的名字发送给服务端 => @setname x0x3. 与谁聊天(呢称-》Lihu) => @Lihu infomation4.如果输入的数据是=> @exit =>表示要断开连接"""import socketimport configwith socket.socket() as client: client.connect(config.addr) while True: data = input(">>>") if not data: continue if data == "@exit": break client.sendall(bytes(data, encoding="utf-8")) recv = client.recv(1024)
转载地址:http://yhezi.baihongyu.com/