一、Socket语法及相关
前言:osi七层模型:
第七层:应用层。
各种应用程序协议,如HTTP,FTP,SMTP,POP3.
第六层:表示层。
信息的语法语义以及它们的关联,如加密解密,转换翻译,压缩解压缩。
第五层:会话层。
不同的机器上的用户之间建立和管理会话。
第四层:传输层。
接受上一层数据在必要的时候把数据进行分割,并将这些数据交给网络层, 且保证这些数据段有效到达对端。
第三层:网络层。
控制子网的运行,如逻辑编址,分组传输,路由选择。IPV4,IPV6,IP
第二层:数据链路层。
物理寻址,同时将原始比特流转变为逻辑传输线路。
第一层:物理层。
机械,电子,定时接口通信道上的原始比特流传输。IEEE802.2 ,Ethernet V.2,Internetwork
ICP/IP 三次握手,四次断开;
UDP 很少使用
Socket概念:
Socket本质上就是2台网络互通的电脑之间,架设一个通道,两台电脑通过这个通道来实现数据的互相传递。
我们知道网络通信都是基于ip+port(ip地址,端口号)方位定位到目标的具体机器上的具体服务,操作系统有0-65535个端口,每个端口都可以独立对外提供服务,如果把一个公司比作一台电脑,那么公司的总机号码就相当于ip地址,每个员工的分机号就相当于端口号,你想找公司某个人,必须先打电话到总机,然后再转分机。
建立一个socket必须至少有2端,一个服务端,一个客户端。服务端被动等待并接收请求,客户端主动发起请求,连接建立之后,双方可以互发数据。
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO,打开,关闭)。
socket和file的区别:
- file模块是针对某个指定文件进行【打开】【读写】【关闭】
- socket模块是针对 服务器端 和 客户端Socket进行 【打开】【读写】【关闭】
socket语法:
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)
参数一:地址簇family
socket.AF_INET IPv4(默认)
socket.AF_INET IPv6
socket.AF_UNIX只能够用于单一的Unix系统进程间通信
参数二:类型type
socket.SOCK_STREAM 流式socket,for TCP(default)
socket.SOCK_DGRAM 数据报式socket,for UDP
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 #注意:利用socket.SOCK_RAW可以用来伪造ip头,从而用来“洪流攻击“。
socket.SOCK_RDM 是一种可靠的UDP形式,级保证交付数据报但不保证顺序。SOCK_RAW用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
socket.SOCK_SEQPACKET 可靠的连续数据包服务
以上type类型,只有SOCK_STREAM,SOCK_DGRAM在所有操作系统上均可用。
参数三:协议proto
0 (默认)与特定的地址家族相关的协议,如果是 0, 则系统就会根据地址格式和套接类别,自动选择一个合适的协议。
The protocol number is usually zero and may be omitted or in the case where the address family is
AF_CAN
the protocol should be one of CAN_RAW
or CAN_BCM
. If fileno is specified, the other arguments are ignored, causing the socket with the specified file descriptor to return. Unlike socket.fromfd()
, fileno will return the same socket and not a duplicate. This may help close a detached socket using socket.close()
.socket实例的常用方法:
sk.bind(address)
s.bind(address)将套接字绑定到地址。address地址的格式取决于地址簇。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5。backlog这个值不能无限大,因为要在内核中维护连接队列。
sk.setblocking(bool)
是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
sk.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
接收TCP客户的连接(阻塞式)等待连接的到来。
sk.connet(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.fileno()
套接字的文件描述符。
二、socket实例
伪代码:
1 伪代码: 2 服务端: 3 import socket #导入socket 4 server=socket.socket() #定义服务端的协议类型,默认为TCP/IP。socket.socket()默认为socket.socket(family=AF_INET,type=SOCK_STREAM) 5 listen('0.0.0.0',80) 设置默认的监听ip+port 6 waiting() 服务端指定端口等待接收来自客户端的信息 7 recv() 接收信息 8 send() 返回信息 9 客户端: 10 import socket #导入socket 11 client=socket.socket() #定义服务端的协议类型,默认为TCP/IP。socket.socket()默认为socket.socket(family=AF_INET,type=SOCK_STREAM) 12 connect((server.ip,server.port)) 13 socket.send(message) 14 socket.recv() 接收信息 15 socket.close()
实例1:实现简单的服务端和客户端的单次数据传输
1 服务端: 2 import socket 3 server=socket.socket() #声明socket类型,同时生成socket连接对象 4 server.bind(('localhost',8888)) #绑定ip,port 5 server.listen() #开始监听 6 print('等待客户端的连接...') 7 conn,addr=server.accept() #接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来... 8 '''conn就是客户端连进来而在服务器端为其生成的一个连接实例''' 9 print('新连接:',addr) #接收客户端的(host,port)到addr 10 data=conn.recv(1024) #一次接受客户端信息为1024bytes 11 print('收到消息:',data) 12 conn.send(data.decode().lower().encode()) #将英文字符串全部转换为小写。由于data是bytes类型,先将bytes类型decode成unicode的str,对字符串进行操作以后,再进行encode()为bytes类型传输 13 '''经过测试data.lower()一样可以执行成功,因此,bytes类型的字符串也可以进行字符串操作''' 14 15 server.close() 16 客户端: 17 import socket 18 client=socket.socket() #默认ipv4,TCP/IP。声明socket类型,同时生成socket连接对象。 19 client.connect(('localhost',8888)) 20 '''连接到address处的套接字。client.connect(address),其中address的格式为元组(hostname.port), 21 如果连接出错,返回socket.error错误。 22 此外,sk.connect_ex(address) 连接成功返回0,连接失败时返回编码,如:10086.''' 23 client.send('Hello World'.encode('utf-8')) #客户端向服务端发送数据 24 data=client.recv(1024) #接收来自服务端的数据,1024的单位是字节,1024 bytes=1 kb 25 print('recv:',data.decode()) 26 client.close()
执行:
实例2:实现服务端和客户端的循环不限次数的网络传输
上面的案例仅实现了单次的访问和返回,并且限定了传输的数据内容。
下面是可以循环输入且可以输入不同类型的优化版本。
1 服务端: 2 import socket 3 server=socket.socket() #声明协议类型 4 ip_port=('localhost',8888) 5 server.bind(ip_port) #绑定ip,port 6 server.listen() # 开始监听 7 print('等待客户端的连接...') 8 9 conn, addr = server.accept() # 接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来... 10 print('新连接:', addr) 11 while True: #多次命令的交互主要在于循环体建立在此,conn实例化的连接已经完成,不在循环体内。 12 print('等待新指令') 13 data = conn.recv(1024) 14 print('收到消息:', data) 15 conn.send(data.lower()) 16 server.close() 17 客户端: 18 import socket 19 client=socket.socket() #默认ipv4,TCP/IP。声明socket类型,同时生成socket连接对象。 20 ip_port=('localhost',8888) 21 client.connect(ip_port) 22 while True: 23 msg = input('>>:').strip() 24 client.send(msg.encode('utf-8')) # 客户端向服务端发送数据 25 data = client.recv(1024) # 接收来自服务端的数据,1024的单位是字节,1024 bytes=1 kb 26 print('recv:', data.decode()) 27 client.close()
实例3:优化:解决输入空信息时阻塞问题
由于在client中发送空数据,server.client(1024)没有接收到数据,它会一直等着收数据,导致程序阻塞。所以client端也要加上对发送的数据是否为空字符集做出判断。
1 服务端: 2 import socket 3 server=socket.socket() #声明协议类型 4 ip_port=('localhost',8888) 5 server.bind(ip_port) #绑定ip,port 6 server.listen() # 开始监听 7 print('等待客户端的连接...') 8 conn, addr = server.accept() # 接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来... 9 print('新连接:', addr) 10 while True: #多次命令的交互主要在于循环体建立在此,conn实例化的连接已经完成,不在循环体内。 11 print('等待新指令') 12 data = conn.recv(1024) 13 print('收到消息:', data) 14 conn.send(data.lower()) 15 server.close() 16 客户端: 17 import socket 18 client=socket.socket() #默认ipv4,TCP/IP。声明socket类型,同时生成socket连接对象。 19 ip_port=('localhost',8888) 20 client.connect(ip_port) 21 while True: 22 msg = input('>>:').strip() 23 if len(msg)==0:continue 24 client.send(msg.encode('utf-8')) # 客户端向服务端发送数据 25 data = client.recv(1024) # 接收来自服务端的数据,1024的单位是字节,1024 bytes=1 kb 26 print('recv:', data.decode()) 27 client.close()
实例4:优化:实现多进程的等待访问服务端
1 服务端: 2 import socket 3 server=socket.socket() 4 ip_port=('localhost',8888) 5 server.bind(ip_port) #绑定ip,port 6 server.listen() # 开始监听 7 print('等待客户端的连接...') 8 while True: 9 conn, addr = server.accept() # 接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来... 10 print('新连接:', addr) 11 while True: 12 print('等待新指令') 13 data = conn.recv(1024) 14 print('收到消息:', data) 15 conn.send(data.lower()) 16 server.close() 17 客户端: 18 import socket 19 client=socket.socket() #默认ipv4,TCP/IP。声明socket类型,同时生成socket连接对象。 20 ip_port=('localhost',8888) 21 client.connect(ip_port) 22 while True: 23 msg = input('>>:').strip() 24 client.send(msg.encode('utf-8')) # 客户端向服务端发送数据 25 data = client.recv(1024) # 接收来自服务端的数据,1024的单位是字节,1024 bytes=1 kb 26 print('recv:', data.decode()) 27 client.close()
问题,在windows上运行该案例,只要client端断开,server端就会强制断开。
如果在linux环境中,连接的client端断开,server会进入死循环,conn.recv(1024)一直收到的都是“ ”(空字符串)。为了解决死循环的问题,可以将server端改为:
1 import socket 2 server=socket.socket() 3 ip_port=('localhost',8888) 4 server.bind(ip_port) #绑定ip,port 5 server.listen() # 开始监听 6 print('等待客户端的连接...') 7 while True: 8 conn, addr = server.accept() # 接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来... 9 print('新连接:', addr) 10 while True: 11 print('等待新指令') 12 data = conn.recv(1024) 13 print('收到消息:', data) 14 if not data:break 15 conn.send(data.lower()) 16 server.close()
在linux环境环境中可以实现server端的切换接入server端。
另外server.listen(5)如果设置了最大等待接入客户端数,在该实例中也不能演示成功,只能在异步中实现。
实例5:一个简单的ssh:解决send.recv(buffersize)在数据缓冲区的问题,实现循环发送,并解决粘包问题。
由于无论是client.recv(buffersize)还是conn.recv(buffersize)都会限定指定的每次传输的字节大小,一般情况下设置的大小为1024-8192字节。因而只要是超过指定的buffersize,就会将剩下未发送的数据留在缓冲区,在下一次收到命令时优先输出缓冲区的数据。会导致命令的错乱。
典型的案例为ssh中一些指定的命令的返回值大于conn.recv(buffersize).
重要的概念“粘包”,即服务端调用send超过2(包含)次时,调用send时,数据并没有立刻被发送给客户端,而是放到了系统的socket的发送缓冲区中,等缓冲区满了、或者数据等待超时了,数据才会被send到客户端。这样就把好几次的小数据拼成了一个大数据,统一发送到客户端了,这样做的目的是为了提高IO利用效率,一次性发送总比连发好几次效率高。但是这并不是我们这里需要的,出现“粘包”,即2次或多次的数据粘在了一起统一发送了。
1 服务端: 2 import socket,os 3 server=socket.socket() 4 ip_port=('localhost',8888) 5 server.bind(ip_port) #绑定ip,port 6 server.listen() # 开始监听 7 print('等待客户端的连接...') 8 while True: 9 conn, addr = server.accept() # 接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来... 10 print('addr:',addr) 11 while True: 12 data = conn.recv(1024) 13 if not data:break 14 res=os.popen(data.decode()).read() 15 length=len(res.encode()) 16 '''为了确保长度不会因为不同的字符编码格式下中英文字符的字节数大小,统一都取格式为bytes的文本字节总长度。''' 17 print('lenth of res:',length) 18 if length==0: 19 res='cmd has no output...' 20 conn.send(str(length).encode()) 21 print('file total size has been send:',length) 22 buffer_recv=conn.recv(1024) 23 print('continue') 24 conn.sendall(res.encode()) 25 print('成功发送命令执行结果') 26 server.close() 27 客户端: 28 import socket 29 client=socket.socket() #默认ipv4,TCP/IP。声明socket类型,同时生成socket连接对象。 30 ip_port=('localhost',8888) 31 client.connect(ip_port) 32 while True: 33 msg = input('>>:').strip() 34 if len(msg)==0:continue 35 client.send(msg.encode('utf-8')) # 客户端向服务端发送数据 36 length=int(client.recv(1024).decode()) 37 print('接收文件的总大小为:',length) 38 client.send('准备好接收数据了...'.encode()) 39 receive_size=0 40 cmd_res=b'' 41 # f=open('test.txt','w') 42 while receive_size != length: 43 data = client.recv(1024) # 接收来自服务端的数据,1024的单位是字节,1024 bytes=1 kb 44 receive_size+=len(data) 45 cmd_res+=data 46 print('文件内容已经成功传输过来:',cmd_res.decode()) 47 client.close()
三、简单的ftp实现:文件的传输
伪代码:
1 ftp server 2 1. 读取文件名 3 2. 检测文件是否存在 4 3. 打开文件 5 4. 检测文件大小 6 5. 发送文件大小给客户端 7 6. 等客户端确认 8 7. 开始边读边发数据 9 8. 发送md5 10 ftp client 11 1. 发送打开文件命令 12 2. 从客户端收到即将接收的文件大小 13 3. 发送给服务端,客户端收到(防止粘包) 14 4. 打开新文件 15 5. 接收文件,边收边写入,边写md5 16 6. 判断收到文件的大小和之前收到的客户端发送的文件大小是否一致,截取最后的md5加密数据 17 7. 关闭文件 18 8. 接收md5 19 9. 最后对两个md5进行比较,确认文件是否完整的传入客户端,且没发生数据粘包情况导致的文件混乱
实例:
1 服务端: 2 import socket,os,hashlib 3 server=socket.socket() 4 ip_port=('localhost',8888) 5 server.bind(ip_port) 6 server.listen(5) 7 while True: 8 conn,addr=server.accept() 9 print('收到客户端的访问信息了...') 10 while True: 11 cmd = conn.recv(1024) 12 print('收到来自客户端的命令', cmd) 13 if not cmd: break # 判断是否为空 14 if cmd.decode().startswith('get'): 15 order, filename = cmd.decode().split() 16 print('客户端想要得到的文件为', filename) 17 if os.path.isfile(filename): 18 f = open(filename, 'rb') 19 file_size = os.stat(filename).st_size 20 conn.send(str(file_size).encode()) 21 print('向客户端发送的文件总大小为', file_size) 22 conn.recv(1024) 23 m = hashlib.md5() 24 count = 0 25 for line in f: 26 m.update(line) 27 conn.send(line) # 边读边传 28 count += 1 29 print('已经向客户端传入文件%s次' % count) 30 f.close() 31 print('文件的md5为', m.hexdigest()) 32 conn.send(m.hexdigest().encode()) 33 print('与该客户端的交互完成,等待下一个客户端的连接') 34 server.close() 35 客户端: 36 import socket,os,hashlib 37 client=socket.socket() 38 client.connect(('localhost',8888)) 39 while True: 40 cmd=input('>>:').strip() 41 if not cmd:continue 42 order,filename=cmd.split() 43 client.send(cmd.encode()) #向客户端发送打开文件的命令 格式指定为get filename 44 #接收即将收到的文件的大小 45 file_size=int(client.recv(1024).decode()) 46 print(file_size) 47 client.send(b'recv file size') 48 received_file_size=0 49 received_file_lines=b'' 50 f=open(filename+'new','wb') 51 m=hashlib.md5() 52 while received_file_size<file_size: 53 if file_size-received_file_size>1024: 54 size=1024 55 else: 56 size=file_size-received_file_size 57 line=client.recv(size) 58 received_file_size+=len(line) 59 received_file_lines+=line 60 f.write(line) 61 m.update(line) 62 else: 63 print('持续接收的文件大小为',received_file_size,'总文件大小为',file_size) 64 print('接收的文件的md5为',m.hexdigest()) 65 f.close() 66 file_md5=client.recv(1024) 67 print('最后接收服务端的md5',file_md5) 68 client.close()
上面的简单的ftp实例已经成功在win7上成功的实现。
主要注意的问题:
客户端和服务端之间的send,recv一定要交互进行,防止粘包。
文件的打开和写入,都需要在正确的时间进行关闭。
文件大小的判断至关重要,否则会导致数据文件错乱等。(注意len()取的是字节格式的数据还是字符串格式的数据,中英文的字符编码会有差异导致出错)
1 while received_file_size<file_size: 2 if file_size-received_file_size>1024: 3 size=1024 4 else: 5 size=file_size-received_file_size 6 line=client.recv(size)
四、Socket Server实现多并发
socket的一个问题是:不支持多用户,多并发。
SocketServer的主要目的是用来实现多并发,多用户。
SocketServer简化了编写network servers的任务,是对socket的一个封装,使它更简单。
种类:
class socketserver.TCPServer(server_address,RequestHandleClass,bind_and_active=True)
class socketserver.UDPServer(server_address,RequestHandleClass,bind_and_active=True)
class socketserver.UnixStreamServer(server_address,RequestHandleClass,bind_and_active=True)
class socketserver.UnixDatagramServer(server_address,RequestHandleClass,bind_and_active=True)
前两种是我们会经常使用的两种,后两种不常用。
There are five classes in an inheritance diagram, four of which represent synchronous servers of four types:
使用socketserver的方法步骤:
第一步:First, you must create a request handler class by subclassing the BaseRequestHandler class and overriding its handle() method; this method will process incoming requests.
你必须自己创建一个请求处理类class RequestHandle,并且这个类要继承BaseRequestHandler,并且还有重写父亲类BaseRequestHandleer里的handle().
第二步:Second, you must instantiate one of the server classes, passing it the servers address and the request handler class.
你必须实例化socketserver.TCPServer/socketserver.UDPServer ,并且传递server ip 和 你上面创建的请求处理类 给这个TCPServer
第三步:Then call the handle_request() or serve_forever() method of the server object to process one or many requests.
调用服务器的handle_request()方法或serve_forever()方法来确定进程中是处理一个还是多个请求。
server.handle_request() #只处理一个请求
server.serve_forever() #处理多个请求,永远执行
最后:Finally, call server_close() to close the socket.
最后调用server_close()关闭服务器。
标准使用格式:
1 import socketserver 2 class MyTCPHandler(socketserver.BaseRequestHandler): #每一个客户端的请求都会实例化一个MyTCPHadndler() 3 def handle(self): #所有和客户端的交互都是handle在处理 4 while True: 5 try: 6 self.data = self.request.recv(1024).strip() 7 print("{} wrote:".format(self.client_address[0])) #格式化客户端的ip地址 8 print(self.data) 9 self.request.send(self.data.upper()) 10 except ConnectionResetError as e: 11 print("err",e) 12 break 13 if __name__ == "__main__": 14 HOST, PORT = "localhost", 9999 15 # Create the server, binding to localhost on port 9999 16 server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) 17 server.serve_forever()
最简单实例应用:利用socketserver,客户端交互与服务端的交互收发数据
1 服务端: 2 import socketserver 3 class MyTCPHandler(socketserver.BaseRequestHandler): #每一个客户端的请求都会实例化一个MyTCPHadndler() 4 def handle(self): #所有和客户端的交互都是handle在处理 5 while True: 6 try: 7 self.data = self.request.recv(1024).strip() # 8 print("{} wrote:".format(self.client_address[0])) #格式化客户端的ip地址 9 print(self.data) 10 if not self.data:break 11 self.request.send(self.data.upper()) 12 except ConnectionResetError as e: 13 print("err",e) 14 break 15 if __name__ == "__main__": 16 HOST, PORT = "localhost", 101 17 # Create the server, binding to localhost on port 9999 18 server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) #ThreadingTCPServer支持多线程 19 server.serve_forever() 20 客户端: 21 import socket 22 client=socket.socket() 23 client.connect(('localhost',101)) 24 while True: 25 cmd=input('>>:').strip() 26 client.send(cmd.encode()) 27 data=client.recv(1024) 28 print('recv:',data.decode()) 29 client.close()
多线程的类的种类有:
- class
socketserver.
ForkingTCPServer 在windows不好用
- class
socketserver.
ForkingUDPServer
- class
socketserver.
ThreadingTCPServer
- class
socketserver.
ThreadingUDPServer