目录
一、客户端/服务器架构
1.硬件C/S架构(打印机)
2.软件C/S架构
互联网中处处是C/S架构
如淘宝网站是服务端,你的浏览器是客户端(B/S架构也是C/S架构的一种)
腾讯作为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频)
C/S架构与socket的关系:我们学习socket就是为了完成C/S架构的开发
二、osi七层
1、引子
须知一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)
如果你要跟别人一起玩,那你就需要上网了,什么是互联网?
互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语
如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。
人们按照分工不同把互联网协议从逻辑上划分了层级,
为何学习socket一定要先学习互联网协议:
首先:本节课程的目标就是教会你如何基于socket编程,来开发一款自己的C/S架构软件
其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的
然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。
2、TCP/IP协议
三、socket
1、socket概述
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
socket和file的区别:
- file模块是针对某个指定文件进行【打开】【读写】【关闭】
- socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】
2、套接字工作流程
3、基本应用
- 1、Python3.5以后只能发送字节,2.7能发送字符串;
- 2、退出只在客户端退出即可,服务端不用退出;
- 3、s.accept()和conn.recv()是阻塞的,如果接收不到消息,程序会停在原地,前提是s----conn这条线没有断开,即连接正常;
- 4、当服务端和一个客户端正在交互时,服务端无法连接新的客户端,只有前一个客户端断开之后,才能接收新的客户端;
- 5、listen(n)--n代表能挂起的链接数,如果n=1,可以链接一个,挂起一个,第三个拒绝;
- 6、服务端出现端口冲突:修改监听端口号;
- 7、如果服务端发送回来的数据长度大于1024,客户端一次无法接受完,就会出现粘包现象;可以修改客户端的接受长度,但是接收到的数据长度大小不等,不能一直修改接收长度,而且计算机底层最多传输1500个包(千兆网卡,万兆可以调整到八千),调的太大也没有用;因此客户端要循环接收数据,直到接收完成;
- 8、send与sendall的区别:send可能一次发不完,sendall是内部调用send循环发送,直到发完为止,尽量用sendall;
import socket'''客户端:发--收'''ip_port = ('127.0.0.1',9999)#封装一个对象s = socket.socket()#链接服务端,如果已经有一个链接,就挂起s.connect(ip_port)while True: # 发送数据 send_data = input('>>:').strip() #如果发送字符串长度为0,就重新输入 if len(send_data) == 0:continue #如果输入exit,客户端退出,不发送数据 if send_data == 'exit': break s.send(bytes(send_data,encoding='utf-8')) #解决粘包问题,先接收带有数据长度的字节 ready_tag = s.recv(1024) ready_tag = str(ready_tag,encoding='utf-8') if ready_tag.startswith('Ready'): #获取待接收数据长度 data_size = int(ready_tag.split('|')[-1]) #发送信号 start_tag = 'Start' s.send(bytes(start_tag, encoding='utf-8')) # 基于数据长度循环接收数据 recv_msg = b'' #空字节 recv_size = 0 while recv_size < data_size: recv_data = s.recv(1024) recv_msg += recv_data recv_size += len(recv_data) print('data size %s ,recv size %s '%(data_size,recv_size)) print(str(recv_msg,encoding='utf-8'))#关闭s.close()
import socketimport subprocess#导入执行命令模块'''服务端:收--发'''#前1024个是系统端口ip_port = ('127.0.0.1',9999)#定义元组#封装一个对象s = socket.socket()#绑定ip+协议+端口,用来唯一标识一个进程,ip_port必须是元组格式s.bind(ip_port)#定义最大可以挂起链接数,Python3默认最大值为128s.listen(5)#循环待机状态,随时接收新的客户端连接while True: #接收客户链接请求,conn相当于一个特定链接,addr是客户端ip+port conn,addr = s.accept() while True:#用来基于一个链接重复收发消息 try: #用来捕捉客户端关闭异常 # 接收数据 recv_data = conn.recv(1024) #如果客户端断开,接收不到数据,就断开当前链接 if len(recv_data) == 0:break #将接收到的命令输出到Windows终端,并执行命令 p = subprocess.Popen(str(recv_data,encoding='utf-8'),shell=True,stdout=subprocess.PIPE) #接收gbk编码格式的返回值 ret = p.stdout.read() if len(ret) == 0: #如果命令错误,返回值为空 send_data = 'cmd error' else: #如果命令成功 send_data = str(ret,encoding='gbk') #发送数据长度 send_data = bytes(send_data,encoding='utf-8') ready_tag = 'Ready|%s' % len(send_data) conn.send(bytes(ready_tag,encoding='utf-8')) #接收发送信号 back_data = conn.recv(1024) back_data = str(back_data,encoding='utf-8') if back_data.startswith('Start'): # 发送数据 conn.send(send_data) except Exception:#如果出现异常 break #断开连接 conn.close()
4、更多功能 :sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)
- 地址簇
socket.AF_INET IPv4(默认)
socket.AF_INET6 IPv6socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
服务端套接字函数
sk.bind(address)
s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5,这个值不能无限大,因为要在内核中维护连接队列;
sk.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。接收TCP 客户的连接(阻塞式)等待连接的到来;
客户端套接字函数
sk.connect(address)
连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
sk.connect_ex(address)
同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
公共用途的套接字函数
sk.close()
关闭套接字
sk.recv(bufsize[,flag])
接受套接字的数据。数据以字节形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。当接收到的数据为空时阻塞。
sk.recvfrom(bufsize[.flag])
与recv()类似,但接收UDP数据,返回值是(data,address)。其中data是包含接收数据的字节,address是发送数据的套接字地址。当接收到的数据为空时不阻塞,会接收到一个空的字节。
sk.send(bytes[,flag])
将字符串数据以字节的方式发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于指定数据的字节大小。即:可能未将指定内容全部发送。
sk.sendall(bytes[,flag])
将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。内部通过递归调用send,将所有内容发送出去。
sk.sendto(bytes[,flag],address)
将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
sk.getpeername()
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
sk.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr,port)
面向锁的套接字方法
sk.setblocking(bool)
是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
sk.settimeout(timeout)
设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
sk.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
sk.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
5、基于UDP的套接字
- udp是无链接的,先启动哪一端都不会报错
# 服务端import socketip_port = ('127.0.0.1',9999)sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0)sk.bind(ip_port)while True: data,(host,port) = sk.recvfrom(1024) print(data,host,port) sk.sendto(bytes('ok', encoding='utf-8'), (host,port))#客户端import socketip_port = ('127.0.0.1',9999)sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0)while True: inp = input('数据:').strip() if inp == 'exit': break sk.sendto(bytes(inp, encoding='utf-8'),ip_port) data = sk.recvfrom(1024) print(data)sk.close()
6、总结
- 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时才会清除缓冲区内容。数据是可靠的,但是会粘包。
四、IO多路复用
1、概述
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。其实就是监视socket对象内部是否发生了变化,链接、收发数据时会发生变化;但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间,异步不能有一点阻塞。
2、select
select(rlist, wlist, xlist, timeout=None)
-
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。
-
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。
Windows Python: #提供: selectMac Python: #提供: selectLinux Python: #提供: select、poll、epoll
注意:网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。
3、伪并发:利用select实现伪同时处理多个Socket客户端请求
import socketimport select'''IO多路复用服务端'''ip_port = ('127.0.0.1',9999)s = socket.socket()s.bind(ip_port)s.listen(5)inputs = [s,]outputs = []msg = {} #{"charlie":[消息1,消息2,]}while True: # 监听sk(服务端)对象,如果sk对象发生变化,表示有客户端来连接了,此时rlist值为【s】 # 监听conn对象,如果conn对象发生变化,表示客户端有消息进来,此时rlist值为【客户端】 rlist,wlist,elist = select.select(inputs,outputs,[],1)#1表示监听时限1s,然后往下执行,再回来循环 print(len(inputs),len(rlist),len(wlist)) for r in rlist: if r == s: # 说明s发生变化,此时列表中只有s,r=s conn,addr = r.accept() #conn也是一个socket对象,每连接一个客户端,就新建一个对应的conn inputs.append(conn) conn.sendall(bytes('hello...',encoding='utf-8')) #客户链接之后,为客户创建一个存放消息的列表 msg[conn] = [] else: # 说明conn变化,有消息过来,此时r=conn try: recv_data = r.recv(1024) print('收到') #判断接收到的消息是否为空 if not recv_data: #如果为空,主动抛出异常 raise Exception('断开连接') else: #所有发过消息的客户端对象加入列表 outputs.append(r) msg[r].append(recv_data) except Exception as ex: #如果客户端关闭,清除接收到的对象和消息 inputs.remove(r) del msg[r] #收发分离,发送部分 for w in wlist: last_msg = msg[w].pop()#上一次接收的消息 resp = bytes('response:',encoding='utf-8') + last_msg w.sendall(resp) #发送完消息,就移除该对象 outputs.remove(w)
import socket'''IO多路复用客户端'''ip_port = ('127.0.0.1',9999)s = socket.socket()s.connect(ip_port)recv_data = s.recv(1024)print(str(recv_data,encoding='utf-8'))while True: inp = input('>>>:') if len(inp) == 0: continue else: s.sendall(bytes(inp,encoding='utf-8')) recv_data = s.recv(1024) print(recv_data.decode())
此处的Socket服务端相比与原生的Socket,他支持当某一个请求不再发送数据时,服务器端不会等待而是可以去处理其他请求的数据。但是,如果每个请求的耗时比较长时,select版本的服务器端也无法完成同时操作。
4、poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket
。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
5、epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
- epoll操作过程需要三个接口,分别如下:
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议
。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 函数是对指定描述符fd执行op操作。
-
-
- - epfd:是epoll_create()的返回值。 - op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。- fd:是需要监听的fd(文件描述符)
-
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
6、IO多路复用的触发方式
-
水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要 有数据可读(描述符就绪)那么水平触发的epoll就立即返回.
-
边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据 可读,但是没有新的IO活动到来,epoll也不会立即返回.
7、selectors模块
import selectorsimport socketsel = selectors.DefaultSelector()def accept(sock, mask): conn, addr = sock.accept() print('accepted', conn, 'from', addr) conn.setblocking(False) # 设置为非阻塞 sel.register(conn, selectors.EVENT_READ, read)def read(conn, mask): try: data = conn.recv(1000) if not data: # windows下断开客户端会报错,Linux下会发送空字符 raise Exception print(data.decode('utf8')) conn.send(data) except Exception as e: print('closing', conn) sel.unregister(conn) conn.close()sock = socket.socket()sock.bind(('localhost', 1234))sock.listen(100)sock.setblocking(False)sel.register(sock, selectors.EVENT_READ, accept)while True: events = sel.select() for key, mask in events: callback = key.data # 监听对象绑定的函数,第一次是sock,第二次是conn callback(key.fileobj, mask) # fileobj表示监听对象
8、总结
blocking(阻塞):全程阻塞
no-blocking(非阻塞):不停地检查,检查过程中CPU可以做其他,但是一旦数据准备好,接受数据的时候是阻塞的状态;
IO multiplexing(IO多路复用):就是我们说的select,poll,epoll,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll, epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
-
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
-
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
asynchronous IO(异步IO):它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
五、socketserver模块
- socketserver内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。
1、ThreadingTCPServer基础
- 创建一个继承自 SocketServer.BaseRequestHandler 的类
- 类中必须定义一个名称为 handle 的方法
- 启动ThreadingTCPServer
import socketserver#服务端class MyServer(socketserver.BaseRequestHandler): def handle(self): conn = self.request conn.send(bytes('欢迎致电... ',encoding='utf-8')) while True: try: recv_data = conn.recv(1024) print("[%s] says %s."%(self.client_address,recv_data.decode())) conn.send(recv_data) except Exception as e: print(e) breakif __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1', 8009), MyServer) #无限循环接收新链接 server.serve_forever()
import socket#客户端ip_port = ('127.0.0.1',8009)s = socket.socket()s.connect(ip_port)#接收欢迎信息welcome_msg = s.recv(1024)print(welcome_msg.decode())while True: send_data = input('>>:').strip() if len(send_data) == 0:continue #如果输入exit,客户端退出,不发送数据 if send_data == 'exit': break s.send(bytes(send_data, encoding='utf-8')) recv_data = s.recv(1024) print(recv_data.decode())
2、作用域
- Python和JavaScript中没有块级作用域,Java和C#中有块级作用域,所以下面的代码,前两种语言可以实现,后两种不可以;
if 1 == 1: name = 'charlie'print(name)#charlie
- Python中是以函数为作用域的,在函数外无法调用函数内的变量;
#无法实现def func(): name = 'charlie'func()print(name)
- Python中有作用域链,又内向外找,如果都没有,就报错;
#作用域链name = 'charlie'def f1(): name = 'a' def f2(): name = 'b' print(name) f2()f1()#b
- Python在函数执行之前,作用域链已经全部确定
name = 'charlie'def f1(): print(name)def f2(): name = 'alex' f1()f2()#charlie
3、面试知识点:列表+lambda表达式+for循环
'''函数在执行之前,函数内部代码不执行,函数中的变量由for循环定义,变量一直被重新赋值,直到循环结束,变量等于最后一个循环的值,列表内部元素是函数,函数加()表示执行函数,得到返回值'''li = [lambda :x for x in range(10)]ret1 = li[0]()ret2 = li[9]()print(ret1)#9print(ret2)#9
#每执行一次循环,函数的参数就被重新赋值,所以函数内部的变量是已经被定好的li = [lambda x=i:x for i in range(10)]ret1 = li[0]()ret2 = li[1]()ret9 = li[9]()print(ret1)#0print(ret2)#1print(ret9)#9
4、ThreadingTCPServer源码剖析
ThreadingTCPServer的类图关系如下:
内部调用流程为:
- 启动服务端程序
- 执行 TCPServer.__init__ 方法,创建服务端Socket对象并绑定 IP 和 端口
- 执行 BaseServer.__init__ 方法,将自定义的继承自SocketServer.BaseRequestHandler 的类 MyRequestHandle赋值给 self.RequestHandlerClass
- 执行 BaseServer.server_forever 方法,While 循环一直监听是否有客户端请求到达 ...
- 当客户端连接到达服务器
- 执行 ThreadingMixIn.process_request 方法,创建一个 “线程” 用来处理请求
- 执行 ThreadingMixIn.process_request_thread 方法
- 执行 BaseServer.finish_request 方法,执行 self.RequestHandlerClass() 即:执行 自定义 MyRequestHandler 的构造方法(自动调用基类BaseRequestHandler的构造方法,在该构造方法中又会调用 MyRequestHandler的handle方法)
5、ForkingTCPServer
- ForkingTCPServer和ThreadingTCPServer的使用和执行流程基本一致,只不过在内部分别为请求者建立 “线程” 和 “进程”。
基本使用:
#ForkingTCPServer只是将 ThreadingTCPServer 实例中的代码:server = SocketServer.ThreadingTCPServer(('127.0.0.1',8009),MyRequestHandler)变更为:server = SocketServer.ForkingTCPServer(('127.0.0.1',8009),MyRequestHandler)
SocketServer的ThreadingTCPServer之所以可以同时处理请求得益于 select 和 os.fork 两个东西,其实本质上就是在服务器端为每一个客户端创建一个进程,当前新创建的进程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。
作业练习:
FTP服务端和客户端:
import osBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #本文件上上层路径USER_DIR = os.path.join(BASE_DIR,'db') #用户信息文件夹路径IP = "127.0.0.1"PORT = 8000
import os,syssys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))import jsonimport hashlibimport subprocessimport socketserverfrom conf import settingsclass MyFtpServer(socketserver.BaseRequestHandler): '''FTP服务端''' max_packet_size = 8192 coding = 'utf-8' def put(self,args): '''接收上传文件''' # args={'cmd': 'put', 'filename': 'blank.mp3', 'filesize': 8247185, 'file_owner': 'charlie'} conn = self.request filesize = args["filesize"] file_name = os.path.join(settings.USER_DIR,args["file_owner"], args["filename"]) if os.path.isfile(file_name): #判断文件是否存在,断点传续 has_recv = os.path.getsize(file_name) # 计算临时文件大小 print('文件已存在,大小%s'%has_recv) conn.sendall(bytes(str(has_recv), encoding=self.coding)) with open(file_name,'ab') as f: while has_recv < filesize: recv_data = conn.recv(self.max_packet_size) f.write(recv_data) has_recv += len(recv_data) print("断点续传,接收完成。。。") self.edit_user_info(args) else: start_tag = 'Start' conn.sendall(bytes(start_tag, encoding=self.coding)) recv_size = 0 with open(file_name,'wb') as f: while recv_size < filesize: recv_data = conn.recv(self.max_packet_size) f.write(recv_data) recv_size += len(recv_data) print("接收完成。。。") self.edit_user_info(args) def edit_user_info(self,args): '''修改用户信息''' file_path = os.path.join(settings.USER_DIR,args["file_owner"],'user_info.json') with open(file_path, 'r') as f1: user_info = json.load(f1) user_info["disk_limit"] = int(user_info["disk_limit"]) - args["filesize"] with open(file_path, 'w') as f2: json.dump(user_info,f2) print(user_info) def register(self,args): '''用户注册''' username = args["username"] password = args["password"] pwd = self.md5(password) user_dir = os.path.join(settings.BASE_DIR,'db', username) os.makedirs(user_dir) filename = os.path.join(user_dir,"user_info.json") with open(filename, 'w') as f: user_info = { "username":username,"password":pwd, "disk_limit":1073741824,} json.dump(user_info,f) self.request.sendall('注册成功'.encode(self.coding)) def md5(self,pwd): '''用户密码加密''' hash = hashlib.md5(bytes("n7u8",self.coding)) hash.update(bytes(pwd,self.coding)) return hash.hexdigest() def login(self,args): '''用户登陆''' username = args["username"] password = args["password"] filename = os.path.join(settings.BASE_DIR,"db",username,"user_info.json") print(filename) with open(filename,'r') as f: user_info = json.load(f) print(user_info,type(user_info)) pwd = user_info["password"] if self.md5(password) == pwd: return '登陆成功' else: return '密码错误' def check_user(self,user_dict): '''验证用户''' conn = self.request username = user_dict["username"] user_dir = os.path.join(settings.BASE_DIR, 'db', username) if os.path.isdir(user_dir): ret = self.login(user_dict) conn.sendall(ret.encode(self.coding))#密码错误或者登陆成功 else: conn.sendall('用户名不存在'.encode(self.coding)) def handle(self): '''服务端主程序''' conn = self.request conn.sendall('欢迎使用... '.encode(self.coding)) while True: try: recv_data = str(conn.recv(self.max_packet_size),self.coding) recv_dic = json.loads(recv_data)#用户命令和基本信息字典 cmd = recv_dic["cmd"] if hasattr(self,cmd): func = getattr(self,cmd) func(recv_dic) else: conn.sendall('Invalid command...'.encode(self.coding)) except Exception as e: print(e) break
import osimport sysimport jsonimport socketBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))sys.path.append(BASE_DIR)LOGIN = { "is_login":False}CURRENT_USER = {}class MyFtpClient: '''FTP客户端''' address_family = socket.AF_INET socket_type = socket.SOCK_STREAM max_packet_size = 8192 coding = 'utf-8' 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 put(self,args): ''' 客户端上传数据 # put f:\blank.mp3 :param args: [命令,文件名] :return: 无 ''' cmd = args[0] filename = args[1] if not os.path.isfile(filename): print("%s is not exists..."%filename) else: filesize = os.path.getsize(filename) head_dic = { "cmd":cmd,"filename":os.path.basename(filename), "filesize":filesize,"file_owner":CURRENT_USER["username"]} print(head_dic) head_json = json.dumps(head_dic) self.socket.sendall(bytes(head_json,encoding=self.coding)) start_tag = self.socket.recv(self.max_packet_size).decode() if start_tag.startswith('Start'): send_size = 0 else: send_size = int(start_tag) with open(filename,'rb') as f: f.seek(send_size) while send_size < filesize: data = f.read(1024) self.socket.sendall(data) send_size += len(data) self.view_bar(send_size, filesize) print("\n上传成功,文件大小:%s 已发送大小:%s"%(filesize,send_size)) def view_bar(self,num,total): ''' 上传进度条 :param num: 每次上传数量 :param total:上传数据总量 :return:None ''' rate = num/total rate_num = int(rate * 100) r = "\r%s%.0f%%"%(">" * rate_num, rate_num) sys.stdout.write(r) def register(self): '''用户注册''' while True: username = input('请输入用户名:').strip() password = input('请输入用密码:').strip() if not username or not password: continue user_info = { "cmd": "register", "username": username, "password": password} send_data = json.dumps(user_info) self.socket.sendall(bytes(send_data,self.coding)) # 发送用户命令字典 recv_data = self.socket.recv(self.max_packet_size).decode(self.coding) if recv_data == '注册成功': print(recv_data) return True def login(self): '''用户登陆''' while True: username = input('请输入用户名(q:退出):').strip() password = input('请输入用密码(q:退出):').strip() if not username or not password: continue user_info = { "cmd": "check_user", "username": username, "password": password} send_data = json.dumps(user_info) self.socket.sendall(bytes(send_data,self.coding)) # 发送用户命令字典 recv_data = self.socket.recv(self.max_packet_size).decode(self.coding) print(recv_data) if recv_data == '密码错误': print(recv_data) continue elif recv_data == '用户名不存在': print(recv_data) break elif recv_data == '登陆成功': global CURRENT_USER CURRENT_USER = user_info return True elif recv_data == 'q':break def check_user(self): '''验证登陆''' while True: inp = input("1、登陆 2、注册 3、退出 >>:").strip() if not inp: continue if inp == '1': ret = self.login() if ret:return True else:continue if inp == '2': ret = self.register() if ret:print('请登录') if inp == '3': break def response(self): '''接收信息并转码''' data = self.socket.recv(self.max_packet_size).decode(self.coding) data = json.loads(data) return data def main(self): '''客户端主程序''' welcome_msg = self.socket.recv(self.max_packet_size) print(welcome_msg.decode()) ret = self.check_user() if ret: while True: inp = input("q:quit >>:").strip() if not inp:continue if inp == 'q':break cmd_list = inp.split() cmd = cmd_list[0] if hasattr(self, cmd): func = getattr(self, cmd) func(cmd_list)