python提高2 - 网络编程socket

一、关于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_INET6IPv6
  • 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: 一般不填默认为0

    protocol参数描述
    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服务器

  1. 创建套接字,绑定套接字到本地IP与端口

    • s.socket(socket.AF_INET,socket.SOCK_STREAM)
    • s.bind(address)
  2. 开始监听连接

    • s.listen(backlog)
  3. 进入循环,不断接受客户端的连接请求

    • s.accept()
  4. 然后接收传来的数据,并发送给对方数据

    • s.recv(bufsize[,flag])
    • s.sendall(string[,flag])
  5. 传输完毕后,关闭套接字

    • s.close()

2. tcp客户端

  1. 创建套接字,连接远端地址

    • socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    • s.connect(address)
  2. 连接后发送数据和接收数据

    • s.sendall(string[,flag])
    • s.recv(bufsize[,flag])
  3. 传输完毕后,关闭套接字

    • 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()方法接受数据时)也可以用上面的这个方法进行强制退出
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云端FFF

所有博文免费阅读,求打赏鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值