文章目录
一、关于socket
1. 两个级别访问的网络服务
- Python 提供了两个级别访问的网络服务。
- 低级别的网络服务支持基本的
socket
,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口的全部方法。 - 高级别的网络服务模块
SocketServer
, 它提供了服务器中心类,可以简化网络服务器的开发。
- 低级别的网络服务支持基本的
2. 什么是socket
Socket
又称"套接字",应用程序(进程)通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。
3. 创建套接字
- Python 中,我们用
socket()
函数来创建套接字(socket对象),语法格式如下:
socket.socket([family[, type[, proto]]])
-
family
: 套接字家族Family参数 描述 socket.AF_UNIX
只能够用于单一的Unix系统进程间通信 socket.AF_INET
服务器之间网络通信 socket.AF_INET6
IPv6 -
type
: 要创建套接字的类型,可以选择是面向连接的还是非连接Type参数 描述 socket.SOCK_STREAM
流式socket,当使用TCP时选择此参数(面向连接) socket.SOCK_DGRAM
数据报式socket,当使用UDP时选择此参数(面向数据报) socket.SOCK_RAW
原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 -
protocol
: 一般不填默认为0protocol参数 描述 socket.IPPROTO_RAW
相当于protocol=255,此时socket只能用来发送IP包,而不能接收任何的数据。发送的数据需要自己填充IP包头,并且自己计算校验和 socket.IPPROTO_IP
相当于protocol=0,此时用于接收任何的IP数据包。其中的校验和和协议分析由程序自己完成。
4. socket对象常用方法
(1)说明
- TCP发送数据时,已建立好TCP链接,所以不需要指定地址,而UDP是面向无连接的,每次发送都需要指定发送给谁
- 服务器与客户端不能直接发送列表,元素,字典等带有数据类型的格式,发送的内容必须是字符串数据
(2)服务器端 Socket 函数
- 下表中s是socket对象
Socket 函数 描述 s.bind(address)
将套接字绑定到地址,在AF_INET下,以 tuple(host,port)
的方式传入,如s.bind((host,port))
。host
是本地主机名,可以用socket.gethostname()
获取,也可以自己随便取一个;port
是本机端口号s.listen(backlog)
开始监听TCP传入连接, backlog
指定在拒绝链接前,它是操作系统可以挂起的最大连接数,该值最少为1,大部分应用程序设为5就够用了s.accpet()
接受TCP链接并返回 (conn,address)元组
,其中conn
是新的套接字对象,可以用来接收和发送数据,address
是链接客户端的地址。
(3) 客户端 Socket 函数
- 下表中s是socket对象
Socket 函数 描述 s.connect(address)
链接到 address
处的套接字,一般address
的格式为tuple(host,port)
,如果链接出错,则返回socket.error
错误s.connect_ex(address)
功能与 s.connect(address)
相同,但成功返回0,失败返回errno
的值
(4)公共函数
-
下表中s是socket对象
Socket 函数 描述 s.recv(bufsize[,flag])
接受TCP套接字的数据,数据以字符串形式返回(收到的是二进制字节码), buffsize
指定要接受的最大数据量,flag
提供有关消息的其他信息,通常可以忽略。s.send(string[, flag])
发送TCP数据,将字符串中的数据发送到链接的套接字(发送的是二进制字节码),返回值是成功发送的字节数量,该数量可能小于string的字节大小(实际发送的字节数受到网络最大传输单元 MTU
限制,不一定能把string全发出去,而且send
方法不会自己重发,需要用户检查并自己完成重发)。s.sendall(string[,flag])
完整发送TCP数据,将字符串中的数据发送到链接的套接字(发送的是二进制字节码),但在返回之前尝试发送所有数据(与 send
方法不同,此方法继续从字符串发送数据,直到所有数据都已发送或发生错误)。成功返回None
;失败则抛出异常,且无法确定成功发送了多少数据。s.recvfrom(bufsize[,flag])
接受UDP套接字的数据u,与 recv()
类似,但返回值是tuple(data, address)
。其中data
是包含接受数据的字符串,address
是发送数据的套接字地址s.sendto(string[, flag], address)
发送UDP数据,将数据发送到套接字, address
形式为tuple(ipaddr, port)
,指定远程地址发送,返回值是发送的字节数s.close()
关闭套接字 s.getpeername()
返回套接字的远程地址,返回值通常是一个 tuple(ipaddr, port)
s.getsockname()
返回套接字自己的地址,返回值通常是一个 tuple(ipaddr, port)
s.setsockopt(level, optname, value)
设置给定套接字选项的值 s.getsockopt(level, optname[, buflen])
返回套接字选项的值 s.settimeout(timeout)
设置套接字操作的超时时间, timeout
是一个浮点数,单位是秒,值为None
则表示永远不会超时。一般超时期应在刚创建套接字时设置,因为他们可能用于连接的操作,如s.connect()
s.gettimeout()
返回当前超时值,单位是秒,如果没有设置超时则返回 None
s.fileno()
返回套接字的文件描述 s.setblocking(flag)
如果flag为0,则将套接字设置为非阻塞模式,否则将套接字设置为阻塞模式(默认值)。非阻塞模式下,如果调用 recv()
没有发现任何数据,或send()
调用无法立即发送数据,那么将引起socket.error
异常。s.makefile()
创建一个与该套接字相关的文件 -
如果配置为阻塞状态(默认)的socket已经建立连接,它在收到连接对方(客户端/服务器)的信息前会阻塞在
recv
那行代码处,但是一旦连接断开,socket就会变为非阻塞状态,并不停返回空的字符串(recv一直返回空,说明连接断开)
二、socket编程思路
1. tcp服务器
-
创建套接字,绑定套接字到本地IP与端口
s.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(address)
-
开始监听连接
s.listen(backlog)
-
进入循环,不断接受客户端的连接请求
s.accept()
-
然后接收传来的数据,并发送给对方数据
s.recv(bufsize[,flag])
s.sendall(string[,flag])
-
传输完毕后,关闭套接字
s.close()
2. tcp客户端
-
创建套接字,连接远端地址
socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(address)
-
连接后发送数据和接收数据
s.sendall(string[,flag])
s.recv(bufsize[,flag])
-
传输完毕后,关闭套接字
s.close()
3. 通信流程示意
-
由于TCP的socket是一个流,因此是不存在“读完了对方发送来的数据”这件事的。必须要每次读到数据之后,根据数据本身来判断当前需要等待的数据是否已经全部收到,来判断是否进行下一个recv。
-
基本上所有的TCP的socket编程都是遵循这样的方法:读入新数据;判断有没有完整的新消息;处理新消息,或者等待更多数据。
三、简单的tcp客户端-服务器示例
1. 客户端
# 使用socket编写客户端,使用TCP/UDP调试工具测试。
# 注意测试时ip为本机ip,调试工具中端口和程序中输入的要一致
import socket
def main():
#创建socket
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # tcp形式的
#链接服务器
#tcp_socket.connect((192.168.1.110,80))
server_ip = input("请输入要链接服务器的ip:")
server_port = int(input("请输入要链接服务器的port:"))
server_addr = (server_ip,server_port)
tcp_socket.connect(server_addr)
#收发数据
while True:
send_data = input("输入要发送的数据:")
tcp_socket.send(send_data.encode("utf-8")) #encode把实际字符转为字节串
#关闭socket
tcp_socket.close()
if __name__=="__main__":
main()
2. 服务器
# 使用socket编写服务器,使用TCP/UDP调试工具测试。
# 注意测试时ip为本机ip,调试工具中端口和程序中输入的要一致
import socket
def main():
# 1.创建socket
tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 2.绑定端口和本地ip
tcp_server_socket.bind(("",8000))
# 3.让socket进入监听模式,允许被动连接
tcp_server_socket.listen(128)
# 循环为多个客户端服务
while True:
print("等待新客户端连接...")
# 4.等待客户端连接,阻塞直到被连接
#accept()返回一个元组,这里拆包成client_socket和client_addr
#client_socket用来和发起请求的客户端通信,这样tcp_server_socket就能不被占用继续监听
#client_addr是一个元组,内容为(“客户端ip”,客户端端口)
client_socket,client_addr = tcp_server_socket.accept()
print("连接成功,客户端ip:{},port:{}".format(client_addr[0],client_addr[1]))
#为一个客户端重复服务
while True:
# 5.收发数据
#接受客户端数据,阻塞直到收到数据
recv_data = client_socket.recv(1024)
print("收到数据:{}".format(recv_data.decode("utf-8")))
#回送数据给客户端
if recv_data:
client_socket.send("======OK======".encode("utf-8"))
#当客户端.close()的时候,也会解阻塞.recv(),而且此时收到的数据为空。
#利用这个条件判断while退出
else:
break
# 6.关闭socket
client_socket.close()
print("已经为这个客户端服务完毕\n")
tcp_server_socket.close()
if __name__=="__main__":
main()
四、tcp案例:文件下载器
- 客户端
import socket
def main():
# 1. 创建socket
tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 2. 获取服务器ip和port
#dest_ip = input("请输入下载服务器的ip:")
#dest_port = int(input("请输入下载服务器的port:"))
dest_ip = "192.168.43.57"
dest_port = 8080
# 3. 链接服务器
tcp_socket.connect((dest_ip,dest_port))
# 4. 获取下载文件的名字
file_name = input("请输入要下载的文件名:")
# 5. 将文件名发送到服务器
tcp_socket.send(file_name.encode("utf-8"))
# 6. 接受文件中的数据
recv_data = tcp_socket.recv(1024*1024) # 1MB
# 7. 保存收到的数据到一个文件中
if recv_data:
#这里用with打开,适用于一定能打开文件的场合,在打开的前提下,如果读写出现异常,可以自动close文件。这里是只写,一定能打开文件(创建文件)。这相当于对write的一个try-except异常检查
with open("[donwload]"+file_name,"wb") as f:
f.write(recv_data)
# 8. 关闭socket
tcp_socket.close()
if __name__=="__main__":
main()
- 服务器
import socket
def send_data_2_client(client_socket,client_addr):
# 1. 接受客户端发来的要下载的文件名
file_name = client_socket.recv(1024).decode("utf-8")
print("请求文件下载请求:{}".format(file_name))
file_content = None
# 2. 打开文件读取数据
try:
f = open(file_name,"rb") #直接以二进制读入,传输时不用encode和decode了
file_content = f.read()
f.close()
except Exception as ret:
print("没有此文件")
# 3. 发送文件内容
if file_content:
client_socket.send(file_content)
def main():
# 1.创建socket
tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 2.绑定端口和本地ip
tcp_server_socket.bind(("",8080))
# 3.让socket进入监听模式,允许被动连接 (128是建议的最大同时监听个数,但是到底最大监听多少还是操作系统决定的,这个值没啥大作用)
tcp_server_socket.listen(128)
while True:
# 4.等待客户端连接,阻塞直到被连接
print("等待新客户端连接...")
client_socket,client_addr = tcp_server_socket.accept()
print("连接成功,客户端ip:{},port:{}".format(client_addr[0],client_addr[1]))
# 5.接受文件名,回传文件数据
send_data_2_client(client_socket,client_addr)
# 6.关闭socket
client_socket.close()
tcp_server_socket.close()
if __name__=="__main__":
main()
- 注意
五、阻塞和非阻塞
1. 两种socket
- socket分为阻塞和非阻塞两种,可以通过
setsockopt
,或者更简单的setblocking
,settimeout
设置- socket创建后,默认为阻塞式
.setblocking(False)
,设置socket对象为非阻塞式
2. 不同的表现
-
阻塞式的socket的
recv
服从这样的规则:- 当缓冲区内有数据时,立即返回所有的数据;
- 当缓冲区内无数据时,阻塞当前程序直到缓冲区中有数据。
-
非阻塞式的socket的
recv
服从的规则则是:- 当缓冲区内有数据时,立即返回所有的数据;
- 当缓冲区内无数据时,产生EAGAIN的错误并返回(在Python中会抛出一个异常)
两种情况都不会返回空字符串,返回空数据的结果是对方关闭了连接之后才会出现的。
-
阻塞式的socket的
accept
服从这样的规则:- 当缓冲区内有数据时,立即返回所有的数据;
- 当缓冲区内无数据时,阻塞当前程序直到缓冲区中有数据。
-
非阻塞式的socket的
accept
服从的规则则是:- 当缓冲区内有数据时,立即返回所有的数据;
- 当缓冲区内无数据时,产生EAGAIN的错误并返回(在Python中会抛出一个异常
BlockingIOError
)
3. 关闭问题
-
做服务器时,往往做一个监听线程,每监听到一个连接就开启一个子线程处理该连接
-
问题在于,如果用阻塞式socket,在监听
accpet
的时候,监听线程就被阻塞了,这样如果想关闭监听,监听线程内部无法受到这个信号,一旦在主线程关闭监听socket,监听线程的accept
就会报错。这个问题可以用三个方法解决- 在监听线程
.start()
前,将其设为.setDeamo()
守护线程,这样一旦主线程结束,监听线程强制结束 - 可以把监听线程设为非阻塞的,while循环进行
accept
,同时捕获BlockingIOError
异常pass掉,这样就能不断做while循环,把结束监听socket的判断放在while条件就行了 - 把监听线程设置为阻塞的,并且设置超时时间,while循环进行
accept
,同时捕获timeout
异常pass掉,类似方法2,只要可以在这个线程中轮询判断终止条件即可
- 在监听线程
-
示例(方法2)
#self.__server_listening是一个开关,可以用UI界面控制
#self.__server_socket是非阻塞的
while self.__server_listening:
try:
client_socket,client_addr = self.__server_socket.accept()
print("连接成功")
#各种处理...
client_socket.close() #关闭客户端服务socket
except BlockingIOError:
pass
- 在其他情境(比如使用
recv()
方法接受数据时)也可以用上面的这个方法进行强制退出