python开发之Socket网络编程
一、SOCKET基础
- 我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,我们经常把socket翻译为套接字,socket是在应用层和传输层(TCP/IP协议族通信)之间的一个抽象层,是一组接口,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。应用程序两端通过“套接字”向网络发出请求或者应答网络请求。可以把socket理解为通信的把手(hand)。
- socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。socket的英文原义是“插槽”或“插座”,就像我们家里座机一样,如果没有网线的那个插口,电话是无法通信的。Socket是实现TCP,UDP协议的接口,便于使用TCP,
UDP。 - SOCK_STREAM : TCP
SOCK_Dgram : UDP - family = AF_INET:服务器之间的通信
family = AF_INET6:服务器之间的通信
family = AF_UNIX:Unix不同进程间通信 - server下的方法:
bind()
listen()
accept()
recv()
send(string)
sendall()
close()
- client下的方法:
connect()
recv()
send(string)
sendall() #传送的内容一定是bytes类型
close()
- 网络通信三要素:IP地址,端口号,传输协议。
二、SOCKET通信流程
1. 流程描述:
- 服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket。
- 服务器为socket绑定ip地址和端口号。
- 服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开。
- 客户端创建socket。
- 客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket。
- 服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直等到客户端返回连接信息后才返回,开始接收下一个客户端连接请求。
- 客户端连接成功,向服务器发送连接状态信息。
- 服务器accept方法返回,连接成功。
- 客户端向socket写入信息(或服务端向socket写入信息)。
- 服务器读取信息(客户端读取信息)。
- 客户端关闭。
- 服务器端关闭。
2. 相关方法及参数介绍:
- 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()
套接字的文件描述符。
三、实例(屌丝追女神的故事)
1. 故事1:(一次通信)
背景:从前,有个屌丝阿武,想追女神阿思,闷骚型,心想:老子就给她一次机会,不把握就算了…
###### server ######
import socket
ip_port = ('127.0.0.1',9997)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(5)
print ('server waiting...')
conn,addr = sk.accept()
client_data = conn.recv(1024)
print (str(client_data,"utf8"))
conn.sendall(bytes('滚蛋!',encoding="utf-8"))
sk.close()
###### client ######
import socket
ip_port = ('127.0.0.1',9997)
sk = socket.socket()
sk.connect(ip_port)
sk.sendall(bytes('俺喜欢你',encoding="utf8"))
server_reply = sk.recv(1024)
print (str(server_reply,"utf8"))
2. 故事2:(多次通信)
背景:又有个屌丝,名叫武二,想追女神阿思,内心强大,臭不要脸,于是…
注意:固然武二脸皮够厚,但是落花有意流水无情,女神不给机会和你聊也白搭(server端也需要有while)。
###### server ######
import socket
ip_port = ('127.0.0.1',8888)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(2)
print ("服务端启动...")
conn,address = sk.accept()
while True:
client_data=conn.recv(1024)
if str(client_data,"utf8")=='exit':
break
print (str(client_data,"utf8"))
server_response=input(">>>")
conn.sendall(bytes(server_response,"utf8"))
conn.close()
###### client ######
import socket
ip_port = ('127.0.0.1',8888)
sk = socket.socket()
sk.connect(ip_port)
print ("客户端启动:")
while True:
inp = input('>>>')
sk.sendall(bytes(inp,"utf8"))
if inp == 'exit':
break
server_response=sk.recv(1024)
print (str(server_response,"utf8"))
sk.close()
3. 故事3:(一个client聊完退出了server才能跟下一个client聊)
背景:某天,女神孤独难耐,决定调整状态想和多人聊一聊找个合适的(虽然不是并发,但是可以在不重新开启server的前提下与多人聊天)。
###### server ######
import socket
ip_port = ('127.0.0.1',8870)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(2)
print ("服务端启动...")
while True:
conn,address = sk.accept()
print(address)
while True:
try:
client_data=conn.recv(1024)
except:
print("意外中断")
break
print (str(client_data,"utf8"))
server_response=input(">>>")
conn.sendall(bytes(server_response,"utf8"))
conn.close()
###### client ######
import socket
ip_port = ('127.0.0.1',8870)
sk = socket.socket()
sk.connect(ip_port)
print ("客户端启动:")
while True:
inp = input('>>>')
if inp == 'exit':
break
sk.sendall(bytes(inp,"utf8"))
server_response=sk.recv(1024)
print (str(server_response,"utf8"))
sk.close()
4. 聊天并发实例:
###### server ######
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
print ("服务端启动...")
while True:
conn = self.request
print (self.client_address)
while True:
client_data=conn.recv(1024)
print (str(client_data,"utf8"))
print ("waiting...")
server_response=input(">>>")
conn.sendall(bytes(server_response,"utf8"))
conn.sendall(client_data)
conn.close()
#print self.request,self.client_address,self.server
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('127.0.0.1',8098),MyServer) #把bind,listen都封装进去了
server.serve_forever()
###### client ######
import socket
ip_port = ('127.0.0.1',8098)
sk = socket.socket()
sk.connect(ip_port)
print ("客户端启动:")
while True:
inp = input('>>>')
sk.sendall(bytes(inp,"utf8"))
server_response=sk.recv(1024)
print (str(server_response,"utf8"))
if inp == 'exit':
break
sk.close()
5. 粘包问题及解决:
- 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
- 此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- 注意:只有TCP有粘包现象,UDP永远不会粘包。
粘包解决:
###### server ######
import socket
import subprocess
sk = socket.socket()
print(sk)
address = ('127.0.0.1',8000)
sk.bind(address)
sk.listen(3)
print('waiting......')
while 1:
conn, addr = sk.accept()
print(address)
while 1:
try:
data = conn.recv(1024)
except Exception:
break
if not data:break
print(str(data,'utf8'))
obj = subprocess.Popen(str(data,'utf8'),shell=True,stdout=subprocess.PIPE)
cmd_result = obj.stdout.read() #cmd_result是bytes类型,是Popen命令按照Windows下的gbk规则编码的
result_len = bytes(str(len(cmd_result)),'utf8')
print('>>>>>>',result_len)
conn.sendall(result_len) #两个send在一起时容易发生粘包现象
conn.recv(1024) #粘包解决方法:用一个recv隔开
conn.sendall(cmd_result)
sk.close()
###### client ######
import socket
sk = socket.socket()
print(sk)
address = ('127.0.0.1',8000)
sk.connect(address)
while True:
inp = input('>>>')
if inp == 'exit':
break
sk.send(bytes(inp,'utf8'))
result_len = int(str(sk.recv(1024),'utf8'))
sk.sendall(bytes('111','utf8')) #解决server端粘包现象,随便发一个什么都行
print(result_len)
data = bytes() #创建一个空的bytes数据
while len(data) != result_len:
recv = sk.recv(1024)
data += recv
print(str(data, 'gbk')) #因为传来的数据是Windows内部用gbk编码的,所以用gbk解码
sk.close()
6. 文件上传实例:
###### server ######
import socket
import os
sk = socket.socket()
print(sk)
address = ('127.0.0.1',8000)
sk.bind(address)
sk.listen(3)
print('waiting......')
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
while 1:
conn, addr = sk.accept()
while 1:
data = conn.recv(1024)
cmd,filename,filesize = str(data,'utf8').split('|')
path = os.path.join(BASE_DIR,'joe',filename)
filesize = int(filesize)
f = open(path,'ab')
has_receive = 0
while has_receive != filesize:
data = conn.recv(1024)
f.write(data)
has_receive += len(data)
f.close()
sk.close()
###### client ######
import socket
import os
sk = socket.socket()
print(sk)
address = ('127.0.0.1',8000)
sk.connect(address)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
while True:
inp = input('>>>').strip() #post|11.jpg
cmd,path = inp.split('|')
path = os.path.join(BASE_DIR,path)
filename = os.path.basename(path)
file_size = os.stat(path).st_size
file_info = 'post|%s|%s' %(filename,file_size)
sk.sendall(bytes(file_info,'utf8'))
f = open(path,'rb')
has_sent = 0
while has_sent != file_size:
data = f.read(1024)
sk.sendall(data)
has_sent += len(data)
f.close()
print('上传成功')
sk.close()
四、socketserver
- 虽说用Python编写简单的网络程序很方便,但复杂一点的网络程序还是用现成的框架比较好。这样就可以专心事务逻辑,而不是套接字的各种细节。SocketServer模块简化了编写网络服务程序的任务。同时SocketServer模块也是Python标准库中很多服务器框架的基础。
- socketserver模块可以简化网络服务器的编写,Python把网络服务抽象成两个主要的类,一个是Server类,用于处理连接相关的网络操作,另外一个则是RequestHandler类,用于处理数据相关的操作。并且提供两个MixIn类,用于扩展 Server,实现多进程或多线程。
Server类:
它包含了种五种server类,BaseServer(不直接对外服务)。TCPServer使用TCP协议,UDPServer使用UDP协议,还有两个不常使用的,即UnixStreamServer和UnixDatagramServer,这两个类仅仅在unix环境下有用(AF_unix)。
==================================================================
参考文献:
https://www.cnblogs.com/yuanchenqi/articles/5692716.html
本篇涉及代码见week7 —> day26, day27.