-
网络通信三要素
- IP地址
- 端口号
- 协议
- UDP:
User Datagram Protocal 用户数据报协议- 面向无连接:传输数据之前源端和目的端不需要建立连接。
- 每个数据报代销限制在64K以内。
- 面向报文的不可靠协议。
- 传输速率快,效率高。
- 现实生活实例:邮局寄信、实时聊天、视频会议。
- TCP:
Transmission Control Protocal 传输控制协议- 面向连接:出书数据之前需要建立连接。
- 在连接过程中进行大量数据传输。
- 通过“三次握手”的方式完成连接,是安全可靠的协议。
- 传输速度慢,效率低。
- UDP:
-
Socket相关方法及参数
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() #套接字的文件描述符
-
简单通信实例:
- 描述:
客户端连接服务器端与服务器进行通信。SOCK_STREAM:TCP
SOCK_Dgram :UDPfamily=AF_INET:服务器之间的通信
family=AF_INET6: ipv6
family=AF_UNIX:unix不同进程间通信 -
socket初始化
sk = socket.socket()
socket方法有默认参数,请看socket类的构造方法class socket(_socket.socket): """A subclass of _socket.socket adding the makefile() method.""" __slots__ = ["__weakref__", "_io_refs", "_closed"] def __init__(self, family=-1, type=-1, proto=-1, fileno=None): # For user code address family and type values are IntEnum members, but # for the underlying _socket.socket they're just integers. The # constructor of _socket.socket converts the given argument to an # integer automatically. if fileno is None: if family == -1: family = AF_INET if type == -1: type = SOCK_STREAM if proto == -1: proto = 0 _socket.socket.__init__(self, family, type, proto, fileno) self._io_refs = 0 self._closed = False
- 服务器端代码
import socket # 创建socket连接 sk = socket.socket() address = ('127.0.0.1', 8000) # 绑定IP、端口 sk.bind(address) sk.listen(3) # 排队人数 print('服务器已启动,等待客户端连接...') while True: conn, addr = sk.accept() print('已建立客户连接, 地址:{0}'.format(addr)) while True: try: # 接收客户端信息 data = conn.recv(1024) except Exception: break if not data: break # 打印信息,将bytes转为unicode编码 print(str(data, 'utf8')) # 等待输入 inp = input('>>>') # 将输入内容发送给客户端,需要将str转为bytes conn.send(bytes(inp, 'utf8')) # 关闭连接 conn.close() sk.close()
- 客户端代码
import socket sk = socket.socket() try: sk.connect(('127.0.0.1', 8000)) except ConnectionRefusedError as e: print(e.strerror) quit(0) print('连接服务器成功.') while True: inp = input('>>>') sk.send(bytes(inp, 'utf8')) data = sk.recv(1024) # 一直等待 print(str(data, 'utf8')) sk.close()
- 执行结果:
- 启动服务器端:
- 启动客户端:
- 服务器端
- 客户端
- 服务器端
- 客户端输入内容,服务器端接收数据。
- 再启动一个客户端:
再启动一个客户端,这个新启动的客户端并不能与服务器通信,因为服务器正在与第一个客户端连接,此时再有客户端连接服务器,就需要排队,第一个客户端关闭后,第二个客户端才能与服务器建立连接通信。
- 启动服务器端:
- 描述:
-
远程执行命令
- 描述:
客户端发送命令,服务器端接收命令后执行,将执行结果返回给客户端。- 使用subprocess模块执行客户端发送过来的命令。
- obj = subprocess.Popen(str(data, 'utf8'), shell=True, stdout=subprocess.PIPE)
使用subprocess.Popen()来执行命令。第一个参数为命令字符串,第二个参数为是否为shell命令,第三个参数为将返回结果输出到哪里,我们输出到PIPE管道里。 - cmd_ret = obj.stdout.read() 从标准输出中读取执行结果。
- 最后将结果cmd_ret发送给客户端。
- 客户端在接收数据的时候一开始只能接收1024字节的数据,如果服务器端发送超过1024字节的数据,客户端将接收不到大于1024字节的数据,就会产生错误的结果,针对这一问题的解决:
- 客户端要循环接收数据,每次接收1024字节的数据,直至接收完毕。
这里的问题在于循环接收必须有一个循环终止的条件。
服务器端发送数据给客户端,客户端根本就不知道服务器端发送过来的数据有多少,也不知道自己究竟要循环接收多少次。服务器端发送完数据,直接阻塞。这其实是一个很蛋疼的问题。针对这个问题的解决方法是:服务器端直接将数据大小发送给客户端,客户端拿这个大小来跟已经接收数据的大小进行比对,如果两者不相等,就继续接收数据,如果相等,就不再接收数据。data = bytes() # 第一次接收包长度 data_len = sk.recv(1024) # 将长度转为int型,接收的数据是bytes型,bytes型不能直接转换int型,所以要加一道工序:将bytes先转换为str,再转换为int。 data_len = int(str(data_len, 'utf8')) # 累加包数据 while len(data) != data_len: data += sk.recv(1024)
- 第2个问题是粘包的问题。
服务器端向客户端先发送大小数据,紧接着就发送具体内容数据,两次发送距离时间很短,两个数据包可能会粘在一起,这就是粘包现象。
conn.send(ret_len) # 发送长度数据 conn.sendall(cmd_ret) # 发送返回结果数据
针对这个问题的解决方法是:服务器端发送完长度数据后,客户端给服务器端一个反馈,服务器端得到反馈,知道客户端已经接收了长度数据,再向客户端发送内容数据。
- 客户端要循环接收数据,每次接收1024字节的数据,直至接收完毕。
- 服务器端:
import socket import subprocess sk = socket.socket() address = ('127.0.0.1', 8000) sk.bind(address) sk.listen(3) # 排队人数 print('服务器已启动,等待客户端连接...') while True: conn, addr = sk.accept() print('已建立客户连接, 地址:{0}'.format(addr)) while True: 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_ret = obj.stdout.read() # 先发送包长度 ret_len = bytes(str(len(cmd_ret)), 'utf8') conn.send(ret_len) # 发送内容数据 conn.sendall(cmd_ret) conn.close() sk.close()
- 客户端:
import socket sk = socket.socket() try: sk.connect(('127.0.0.1', 8000)) except ConnectionRefusedError as e: print(e.strerror) quit(0) print('连接服务器成功.') while True: inp = input('>>>') sk.send(bytes(inp, 'utf8')) data = bytes() # 第一次接收包长度 data_len = sk.recv(1024) data_len = int(str(data_len, 'utf8')) # 累加包数据 while len(data) != data_len: data += sk.recv(1024) print(str(data, 'gbk')) sk.close()
- 执行结果
- 启动服务器端
- 启动客户端
- 客户端输入dir命令(返回字节不超过1024)
- 客户端输入ipconfig/all命令(返回字节超过1024)
可以看出已经可以显示出全部的结果。
- 启动服务器端
- 描述:
-
向服务器上传文件
- 描述
在客户端中输入"post|文件路径"可以将文件上传至服务器uploads目录中。例如:要上传的文件为a.txt,那么输入命令:post|a.txt
我了简单期间,我们将文件放在目录下,目录结构如下:- 客户端:
- 分解命令'post|***.jpg'
- 获取文件绝对路径
- 获取文件大小
- 将文件名、文件大小数据发送给服务器端
# 获取目录的绝对路径 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) cmd, path = inp.split('|') # 分解命令字符串 path = os.path.join(BASE_DIR, path) # 拼接路径获取文件绝对路径 file_name = os.path.basename(path) # 获取文件名 file_size = os.path.getsize(file_name) # 获取文件大小 file_info = 'post|%s|%s' %(file_name, file_size) # 拼接文件信息数据 sk.sendall(bytes(file_info, 'utf8')) # 将文件信息先发送给服务器端
- 以二进制读‘rb’打开文件
- 循环发送文件,直至发送完毕
循环终止的条件为:已发送数据数量=文件大小
- 服务器端:
- 服务器先接收到客户端发送的文件信息
- 在服务器指定位置以写二进制方式打开文件
- 循环接收二进制,并在服务器端写入文件
- 写入文件成功,发送上传成功信息给客户端
- 客户端:
- 服务器端
# _*_ coding: utf-8 _*_ __author__ = 'jevoly' import socket import os sk = socket.socket() address = ('127.0.0.1', 8000) sk.bind(address) sk.listen(3) # 排队人数 print('服务器已启动,等待客户端连接...') BASE_DIR = os.path.dirname(os.path.abspath(__file__)) while True: conn, addr = sk.accept() print('已建立客户连接, 地址:{0}'.format(addr)) while True: data = conn.recv(1024) # 先接收文件信息 # 分解文件信息为:命令、文件名、文件大小 cmd, file_name, file_size = str(data, 'utf8').split('|') file_size = int(file_size) # 将文件大小转成int型 # 拼接文件上传路径,这里是uploads文件夹 path = os.path.join(BASE_DIR, 'uploads', file_name) received = 0 # 已接收数据量 try: f = open(path, 'wb') # 打开文件(新建) while file_size != received: data = conn.recv(1024) # 接收文件数据 f.write(data) # 写入数据 received += len(data) # 累加已接收数据量 except Exception as e: print(e.strerror) finally: f.close() # 关闭文件 # 上传文件成功,发送通知给客户端 success = '上传"{0}"成功,共{1}KB!'.format(file_name, file_size / 1000) print(success) conn.sendall(bytes(success, 'utf8')) conn.close() sk.close()
- 客户端
# _*_ coding: utf-8 _*_ __author__ = 'jevoly' import socket import os # 获取目录的绝对路径 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) sk = socket.socket() try: sk.connect(('127.0.0.1', 8000)) except ConnectionRefusedError as e: print(e.strerror) quit(0) print('连接服务器成功.') while True: inp = input('>>>').strip() # post|11.png cmd, path = inp.split('|') path = os.path.join(BASE_DIR, path) # 拼接路径获取文件绝对路径 file_name = os.path.basename(path) file_size = os.path.getsize(file_name) file_info = 'post|%s|%s' %(file_name, file_size) sk.sendall(bytes(file_info, 'utf8')) try: # 打开本地文件 f = open(path, 'rb') sent = 0 while sent != file_size: data = f.read(1024) sk.send(data) sent += len(data) except Exception as e: print(e.strerro) finally: f.close() # 接收服务器端发送的数据并显示 data = sk.recv(1024) print(str(data, 'utf8')) sk.close()
-
执行结果
- 描述
-
并发聊天
- 描述
服务器端可以同时接收多个客户端建立通讯连接。
需要是使用:socketserver模块
- SocketServer模块:
- SocketServer模块简化了编写网络服务程序的任务,同时SocketServer模块也是python标准库中很多服务器框架的基础。
socketserver模块可以简化服务器的编写,python把网络服务抽象成两个主要的类,一个是Server类,用于处理连接相关的网络操作;另一个是RequestHandler类,用于处理数据相关的操作。并且提供两个MixIn类,用于扩展Server,实现多进程、多线程。 - Server类:
包含5种Server类。BaseServer(不直接对外服务)。TCPServer使用TCP协议,UDPServer使用UDP协议,还有两个不常使用的UnixStreamServer和UnixDatagramServer,这两个类仅仅在unix环境下有用(AF_unix)。 - 创建一个socketserver至少有以下几步:
- 创建request handler类,必须继承BaseRequestHandler类,并且必须重写handler()方法,并将业务逻辑写在handler()方法里。
class MyServer(socketserver.BaseRequestHandler): def handle(self): pass
- 必须通过服务器地址和request handler类实例化一个server类。
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8000), MyServer)
- 然后调用服务器对象的handler_request()或者serve_forever()方法处理一个或多个请求。
server.serve_forever()
- 最后,调用server_close()关闭socket。
- 创建request handler类,必须继承BaseRequestHandler类,并且必须重写handler()方法,并将业务逻辑写在handler()方法里。
- SocketServer模块简化了编写网络服务程序的任务,同时SocketServer模块也是python标准库中很多服务器框架的基础。
- 客户端
客户端不需要修改 - 服务器端
服务器端需要实现并发# _*_ coding: utf-8 _*_ __author__ = 'jevoly' import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): # 业务逻辑 while True: conn = self.request print('已建立客户连接, 地址:{0}'.format(self.client_address)) while True: client_data = conn.recv(1024) print(str(client_data, 'utf8')) print('waiting...') inp = input('>>>') conn.sendall(bytes(inp, 'utf8')) conn.close() pass if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1', 8000), MyServer) print('服务器已经启动,等待客户端连接...') server.serve_forever()
- 执行结果:
- 描述
python socket网络编程日记
最新推荐文章于 2025-01-20 09:26:03 发布