1、socketpython
定义:编程
socket本质上就是在2台网络互通的电脑之间,架设一个通道,两台电脑经过这个通道来实现数据的互相传递。 咱们知道网络 通讯 都 是基于 ip+port(套接字) 方能定位到目标的具体机器上的具体服务,操做系统有0-65535个端口,每一个端口均可以独立对外提供服务,若是 把一个公司比作一台电脑 ,那公司的总机号码就至关于ip地址, 每一个员工的分机号就至关于端口, 你想找公司某我的,必须 先打电话到总机,而后再转分机 。 数组
socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操做抽象为几个简单的接口供应用层调用已实现进程在网络中通讯。服务器
在TCP/IP协议中,TCP协议(传输层)经过三次握手创建一个可靠的链接网络
第一次握手:客户端尝试链接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认数据结构
第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态多线程
第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手并发
定睛一看,服务器socket与客户端socket创建链接的部分其实就是大名鼎鼎的三次握手app
创建一个socket必须至少有2端, 一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求, 链接创建以后,双方能够互发数据。 socket
服务器端:
1 #服务器端 2 import socket 3 server=socket.socket(地址簇,套接字) #声明 4 5 server.bind(("localhost",9999)) #肯定监听的端口 6 7 server.listen() #开始监听 8 9 conn,addr=server.accpet() #在服务器端生成实例并赋值 10 print("new conn:",addr) 11 12 while True: #服务器端循环接收数据 13 14 data=conn.recv(1024) #接受数据,最多1K 15 print(data) # >>b'abc' 16 17 conn.send(data.upper) #将数据改成大写发回去
客户端:
1 #客户端 2 import socket 3 4 client=socket.socket() #实例化 5 6 client.connect("localhost",9999) #绑定IP和端口 7 8 client.send(b"abc") #转为二进制发送 9 10 data=client.recv(1024) 11 12 print(data) 13 14 >> b'ABC'
客户端:
1 #服务器端 2 import socket,os 3 server=socket.socket(地址簇,套接字) #声明 4 5 server.bind(("localhost",9999)) #肯定监听的端口 6 7 server.listen() #开始监听 8 9 conn,addr=server.accpet() #在服务器端生成实例并赋值 10 print("new conn:",addr) 11 12 while True: #服务器端循环接收数据 13 14 data=conn.recv(1024) #接受数据,最多1K 15 data=data.decode() #二进制转成'utf-8' 16 if no data : 17 print('客户端已断开') 18 break 19 print('执行指令:',data) 20 21 cmd_res=os.popen(data).read() #执行操做 22 23 conn.send(len(cmd_res).encode('utf-8')) #先发送数据的大小 24 25 time.sleep(0.5) #先睡0.5s 防止粘包,另外一种方法conn.recv(),客户端conn.sendall('123') 26 27 conn.send(cmd_res.encode('utf-8')) #将数据发回去 28 29 server.close() #我好像前面都忘写关闭了
客户端:
1 #客户端 2 import socket 3 4 client=socket.socket() #实例化 5 6 client.connect("localhost",9999) #绑定IP和端口 7 8 while True: 9 cmd = input(">>:").strip() 10 11 client.send(cmd.encode('utf-8')) 12 13 cmd_res_size=(client.recv(1024)).decode() #要传输的文件的大小 14 received_size=0 #本地已有的文件大小 15 received_data=b'' 16 print("命令结果大小",cmd_res_size) 17 while received_size<cmd_res_size: 18 data=client.recv(1024) 19 received_data+=data 20 received_size+=len(data) 21 else: 22 print("数据接收完毕") 23 print(received_data.decode()) 24 25 client.close()
小Tips:
1 import hashlib 2 3 m=hashlib.md5 4 5 m.updata(b'abc') 6 m.updata(b'123') 7 8 #m.updata(b'abc123') #二者结果一毛同样,so能够采用这种方式读取文件计算md5值 9 10 print(m.hexdigest) #16进制
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)
参数一:地址簇
socket.AF_INET IPv4(默认)
socket.AF_INET6 IPv6socket.AF_UNIX 只可以用于单一的Unix系统进程间通讯
参数二:类型
socket.SOCK_STREAM 流式socket , for TCP (默认)
socket.SOCK_DGRAM 数据报式socket , for UDPsocket.SOCK_RAW 原始套接字,普通的套接字没法处理ICMP、IGMP等网络报文,而SOCK_RAW能够;其次,SOCK_RAW也能够处理特殊的IPv4报文;此外,利用原始套接字,能够经过IP_HDRINCL套接字选项由用户构造IP头。
socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在须要执行某些特殊操做时使用,如发送ICMP报文。SOCK_RAM一般仅限于高级用户或管理员运行的程序使用。
socket.SOCK_SEQPACKET 可靠的连续数据包服务参数三:协议
0 (默认)与特定的地址家族相关的协议,若是是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
sk.bind(address)
s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
开始监听传入链接。backlog指定在拒绝链接以前,能够挂起的最大链接数量。
backlog等于5,表示内核已经接到了链接请求,但服务器尚未调用accept进行处理的链接个数最大为5
这个值不能无限大,由于要在内核中维护链接队列
sk.setblocking(bool)
是否阻塞(默认True),若是设置False,那么accept和recv时一旦无数据,则报错。
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()相似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
sk.send(string[,flag])
将string中的数据发送到链接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容所有发送。
sk.sendall(string[,flag])
将string中的数据发送到链接的套接字,但在返回以前会尝试发送全部数据。成功返回None,失败则抛出异常。
内部经过递归调用send,将全部内容发送出去。
sk.sendto(string[,flag],address)
将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
sk.settimeout(timeout)
设置套接字操做的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。通常,超时期应该在刚建立套接字时设置,由于它们可能用于链接的操做(如 client 链接最多等待5s )
sk.getpeername()
返回链接套接字的远程地址。返回值一般是元组(ipaddr,port)。
sk.getsockname()
返回套接字本身的地址。一般是一个元组(ipaddr,port)
sk.fileno()
套接字的文件描述符
I/O多路复用指:经过一种机制,能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做。
Linux中的 select,poll,epoll 都是IO多路复用的机制。
select
select最先于1983年出如今4.2BSD中,它经过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程能够得到这些文件描述符从而进行后续的读写操做。
select目前几乎在全部的平台上支持,其良好跨平台支持也是它的一个优势,事实上从如今看来,这也是它所剩很少的优势之一。
select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制,在Linux上通常为1024,不过能够经过修改宏定义甚至从新编译内核的方式提高这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增加。同时,因为网络响应时间的延迟使得大量TCP链接处于非活跃状态,但调用select()会对全部socket进行一次线性扫描,因此这也浪费了必定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差异,可是poll没有最大文件描述符数量的限制。
poll和select一样存在一个缺点就是,包含大量文件描述符的数组被总体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增长而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,若是进程没有对其进行IO操做,那么下次调用select()和poll()的时候将再次报告这些文件描述符,因此它们通常不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具有了以前所说的一切优势,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll能够同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,若是咱们没有采起行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,可是代码实现至关复杂。
epoll一样只告知那些就绪的文件描述符,并且当咱们调用epoll_wait()得到就绪文件描述符时,返回的不是实际的描述符,而是一个表明就绪描述符数量的值,你只须要去epoll指定的一个数组中依次取得相应数量的文件描述符便可,这里也使用了内存映射(mmap)技术,这样便完全省掉了这些文件描述符在系统调用时复制的开销。
另外一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。
Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。
对于select方法:
句柄列表
11
, 句柄列表
22
, 句柄列表
33
=
select.select(句柄序列
1
, 句柄序列
2
, 句柄序列
3
, 超时时间)
参数: 可接受四个参数(前三个必须)
返回值:三个列表
select方法用来监视文件句柄,若是句柄发生变化,则获取该句柄。
1
、当 参数
1
序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值
1
序列中
2
、当 参数
2
序列中含有句柄时,则将该序列中全部的句柄添加到 返回值
2
序列中
3
、当 参数
3
序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值
3
序列中
4
、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
当 超时时间 =
1
时,那么若是监听的句柄均无任何变化,则select会阻塞
1
秒,以后返回三个空列表,若是监听的句柄有变化,则直接执行。
利用select监听终端操做
import select import sys while True: readable, writeable, error = select.select([sys.stdin,],[],[],1) if sys.stdin in readable: print 'select get stdin',sys.stdin.readline()
利用select实现伪同时处理多个Socket客户端请求
服务器端:
import socket import select sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sk1.bind(('127.0.0.1',8002)) sk1.listen(5) sk1.setblocking(0) # flag为0则为非堵塞模式不然, 套接字将设置为阻塞模式(默认值)。 # 在非阻塞模式下, 若是recv()调用没有发现任何数据或者send()调用没法当即发送数据, 那么将引起socket.error异常。在阻塞模式下, 这些调用在处理以前都将被阻塞。 inputs = [sk1,] # 本身也要监测呀,由于sk1自己也是个socket客户端 while True: readable_list, writeable_list, error_list = select.select(inputs, [], inputs, 1) # 交给select去监控 for r in readable_list: if sk1 == r: # 当客户端第一次链接服务端时 print('链接上服务器') request, address = r.accept() request.setblocking(0) # 设置为非堵塞 inputs.append(request) # 将新链接加入input中监听 else: # 当客户端链接上服务端以后,再次发送数据时 received = r.recv(1024) # 当正常接收客户端发送的数据时,没有数据时会堵塞在这 if received: print('received data:', received) else: # 当客户端关闭程序时 inputs.remove(r) sk1.close()
1)堵塞与非堵塞
一辆从 A 开往 B 的公共汽车上,路上有不少点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于须要下车的人,如何处理更好?
1. 司机过程当中定时询问每一个乘客是否到达目的地,如有人说到了,那么司机停车,乘客下车。 ( 相似阻塞式 )
2. 每一个人告诉售票员本身的目的地,而后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 ( 相似非阻塞 )
很显然,每一个人要到达某个目的地能够认为是一个线程,司机能够认为是 CPU 。在阻塞式里面,每一个线程须要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每一个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒确定不会阻塞。
非阻塞的原理:
把整个过程切换成小的任务,经过任务间协做完成。
由一个专门的线程来处理全部的 IO 事件,并负责分发。
事件驱动模型机制:
事件到的时候触发,而不是同步的去监视事件。
2)事件驱动模型
目前大部分的UI编程都是事件驱动模型,如不少UI平台都会提供onClick()事件,这个事件就表明鼠标按下事件。事件驱动模型大致思路以下:
1. 有一个事件(消息)队列;
2. 鼠标按下时,往这个队列中增长一个点击事件(消息);
3. 有个循环,不断从队列取出事件,根据不一样的事件,调用不一样的函数,如onClick()、onKeyDown()等;
4. 事件(消息)通常都各自保存各自的处理函数指针,这样,每一个消息都有独立的处理函数;
客户端:
import socket ip_port = ('127.0.0.1',8002) sk = socket.socket() sk.connect(ip_port) while True: inp = input('please input:') sk.sendall(inp.encode('utf-8')) sk.close()
SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每一个客户端请求链接到服务器时,Socket服务端都会在服务器是建立一个“线程”或者“进程” 专门负责处理当前客户端的全部请求。
实现多并发知足条件:
1.你必须本身建立一个请求处理类,而且这个类要继承BaseRequestHandler,而且还有重写父亲类里的handle()
2.你必须实例化TCPServer ,而且传递server ip 和 你上面建立的请求处理类 给这个TCPServer
3.server.handle_request() 只处理一个请求
4.server.serve_forever() 处理多个一个请求,永远执行
import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): #条件1,本身建立一个类 def handle(self): #重写父类中的handle() while True: try: self.data = self.request.recv(1024).strip() print("{} wrote:".format(self.client_address[0])) print(self.data) self.request.send(self.data.upper()) except ConnectionResetError as e: print("err",e) break if __name__ == "__main__": HOST, PORT = "localhost", 9999 # Create the server, binding to localhost on port 9999 server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) #实例化ThreadingTCPServer,并传递(端口号+IP),和请求类名 server.serve_forever() #关闭