zoukankan      html  css  js  c++  java
  • python网络通信 --- socket

    socket

    socket 通常被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

    Python标准库提供了socket模块来实现这种网络通信。实例化一个socket类便能得到一个socket对象sock = socket.socket(),使用这个socket对象就可以进行通信了。常用的socket有两种。

    SOCK_STREAM 面向连接的流式socket,基于TCP协议
    SOCK_DGRAM 无连接的数据报式socket,基于UDP协议

    相同类型的socket才能正常的通信,因为他们都有各自发送和接收消息的协议。

    socket对象

    import socket
    s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, fileno=None)

    实例化时指定对应的参数可以得到不同类型的socket,默认使用IPV4和TCP协议的类型

    参数 可选值 说明
    family socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
      socket.AF_INET 默认使用IPv4协议
      socket.AF_INET6 使用IPv6协议
    type socket.SOCK_STREAM 面向连接的流式socket,基于TCP协议
      socket.SOCK_DGRAM 无连接的数据报式socket,基于UDP协议

    实践

    通过写一个聊天的服务器和客户端体验这种通信

    TCP服务端

    使用socket构建一个最简单TCP服务器可接收客户端的连接。我们需要一个socket用于网络通信,并监听一个地址和端口,等待其他的网络连接访问该端口,代码如下。

    server = socket.socket()  # 创建
    
    server.bind(('127.0.0.1', 8000))   # 绑定本机地址和端口
    
    server.listen() # 开始监听端口
    
    # 阻塞等待客户端的连接,连接后返回一个新的可与客户端通信的socket和客户端的(ip,port)
    s, raddr = server.accept()

    当执行上面的python程序后,操作系统将会启动一个进程,该服务进程正在监听8000端口,在Windows命令行中使用netstat -anp tcp | findstr 8000查询监听状态。在Linux上可以使用ss -tanl | grep 8000命令查看。

    C:Usersuser>netstat -anp tcp | findstr 8000
    TCP    127.0.0.1:8000         0.0.0.0:0              LISTENING

    下面构建一个完整的TCP服务器。这是基本的服务器和客户端通信结构图。根据结构图构建聊天服务器

    简单步骤和思路

    • 创建socket
    • 绑定一个ip地址和端口
    • 开始监听(listen)
    • 阻塞等待连接(accept)
    • 客户端连接到来后,开启新线程与该客户端交互,发送和接收消息。(recv和send)
    • 同时我们使用主线程操作服务端退出。

    通过以上分析,我们需要使用多线程,分别与服务器交互,等待客户端连接,与一个连接后的客户端交互;每当成功的连接一个客户端,都需要新启动一个线程进行交互。

    import socket
    import threading
    
    class Server:
        def __init__(self, ip='127.0.0.1', port=8000):  # 设置默认值
            self.addr = ip, port
            self.lock = threading.Lock()
            self.sock = socket.socket()
            self.sock.bind(self.addr)
            self.socks = {"accept": self.sock}  # 将所有创建的socket都放字典,方便释放
    
        def start(self):  # 启动接口
            self.sock.listen()
            threading.Thread(target=self.accept, name="accept", daemon=True).start()
    
        def accept(self):  # 该线程等待连接并创建处理线程
            while True:
                s, raddr = self.sock.accept()
                with self.lock:
                    self.socks[raddr] = s
                threading.Thread(target=self.recv, args=(s, raddr), name="recv", daemon=True).start()
    
        def recv(self, s, raddr):  # 每个客户端开启一个线程与其交互
            while True:
                data = s.recv(1024).decode()
                if data.strip() == "" or data.strip() == "quit":  # 客户端结束条件
                    with self.lock:
                        self.socks.pop(raddr)
                        s.close()
                        break
                print(data)
                s.send("server:{}
    ".format(data).encode())
    
        def stop(self):
            with self.lock:
                for s in self.socks.values():
                    s.close()
    s = Server()
    s.start()
    
    while True:
        cmd = input("server commond:>>>")
        if cmd == "quit":  # 服务器退出条件
            s.stop()
            break
        print(threading.enumerate())

    我们需要注意的问题:

    1. 服务端需要与多个不同客户端进行交互,所以我们需要开启不同线程去处理各自的业务,
    1. 为了服务端在启动后可以获得控制权,我们使用主线程来与服务器管理者交互,使用命令行输入指令就能在服务器启动后与服务器做一些交互,例如代码中的强制关闭服务器,并在强制关闭服务前提前关闭掉这些socket对象。
    1. 在遍历字典来关闭socket对象时,我们使用了锁,要求在这个遍历操作完成前,其他线程无法进行增加或者删除操作,保证了字典遍历时的线程安全。

    socket常用的方法

      方法 含义
    服务端 s.bind(address) 将套接字绑定到地址,以元组(host,port)的形式表示地址
      s.listen(backlog) 开始监听TCP传入连接。backlog:操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了
      s.accept() 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址,为一个元组
    客户端socket函数 s.connect(address) 连接到address处的套接字,格式为元组(hostname,port),如果连接出错,返回socket.error错误
      s.connect_ex(adddress) 功能与connect(address)相同,但是成功返回0,失败返回errno的值
    公共socket函数 s.recv(bufsize[,flag]) 从s接受bytes类型的数据,有数据就接受返回,bufsize指定要接收的最大数据量
      s.send(bytes[,flag]) TCP发送数据。将bytes中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于bytes的字节大小
      s.sendall(bytes[,flag]) 发送全部TCP数据。将bytes中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常
      sendfile() 使用os.sendfile()高效的发送文件的方法,必须使用SOCK_STREAM类型的套接字才能使用
      s.recvfrom(bufsize[.flag]) 接受UDP套接字的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的bytes,address是发送方地址
      s.sendto(string[,flag],address) 发送UDP数据。address是形式为(ipaddr,port)的元组。返回值是发送的字节数
         
      s.getpeername() 返回连接套接字的远程地址(ipaddr,port)
      s.getsockname() 返回套接字自己的地址(ipaddr,port)
      s.setsockopt(level,optname,value) 设置给定套接字选项的值
      s.getsockopt(level,optname[.buflen]) 返回套接字选项的值
      s.settimeout(timeout) 设置套接字操作的超时间,值为None表示没有超时期。一般超时期在创建时设置
      s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None
      s.fileno() 返回套接字的文件描述符
      s.setblocking(flag) 设置阻塞模式,非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
      s.makefile() 创建一个与该套接字相关连的文件,返回一个类文件对象,可是使用文件操作发送和接收数据

    sendfile是一个高效的传送方式,文件数据始终处于内核态,在操作系统缓冲区直接发送,不会到应用层缓冲区。

    使用makefile方法将返回该socket对应的文件对象(io.TextIOWrapper),该对象的write()等价于send()方法, read方法等价于recv(),还可以使用readline等方法。这样我们可以使用文件的接口去收发信息,客户端将使用这种方式与服务器交互。

    sock = socket.socket()
    file = sock.makefile("rw")  # mode="rw" 可读可写
    
    data = file.read()   # 等价于socket.recv()
    
    data = file.read(10) # 指定读取字符大小长度,满10个字符才会返回。
    data = file.readlin()   # 每次读取一行,遇到换行符才返回。
    # 写入数据
    msg = "hello world"
    file.write(msg)
    file.flush()      # 手动flush,否则在缓冲区满或者退出时自动才写入socket。同文件写入操作

    TCP客户端

    相比于服务端,客户端只需要连接服务器后发送和接受消息即可,相对更容易实现。

    客户端需要同时接受和发送消息,而这两个操作均会阻塞,所以两个功能需要在不同的线程。下面代码使用了socket的makefile()方法,使用文件对象进行收发数据。

    import socket
    import threading
    import datetime
    
    class Client:
        def __init__(self, rip, rport):  # 服务器ip 和 端口
            self._raddr = rip, rport
            self._sock = socket.socket()
            self._connect()
    
        def _connect(self):
            self._sock.connect(self._raddr)   # 尝试连接指定的地址
            self.f = self._sock.makefile("rw")
            self.f.write("i am client at {}
    ".format(self._sock.getsockname()))
            self.f.flush()
            threading.Thread(target=self.recv, name="recv", daemon=True).start()  # 一个进程接收消息
            self.send()   # 主进程发送消息
    
        def send(self):
            while True:
                msg = input(">>>").strip()
                self.f.write(msg)
                self.f.flush()
                if msg == "quit":
                    self.stop()
                    break
    
        def recv(self):
            while True:
                msg = self.f.readline()
                print("server:{}{:%Y/%m/%d %H:%M:%S}
    	{}".format(self._sock.getpeername(), datetime.datetime.now(), msg))
    
        def stop(self):
            self.f.close()
            self._sock.close()
    
    c = Client("127.0.0.1", 8000)

    客户端使用connect()方法将会尝试连接服务器(这个服务必须存在,否则无法连接),由于服务基于TCP协议,所以在connect()连接时候,实际上会进行TCP三次握手的连接,但是我们在应用层面无法感知到这个下层行为。同样的在进行close关闭socket时,在断开连接前将会进行四次挥手操作。

     

    使用makefile后会得到该socket的文件对象,在进行read和write时会先将数据放入缓冲区暂存,write方法对应一个发送缓冲区,将需要发送到对方的数据暂存到该缓冲区,在调用flush时才会将数据发送,当写入缓冲区满了而没有及时发送数据,发送数据没有缓存空间可用,将会发生阻塞等待。同样read方法对应一个读取缓冲区,每次从读取缓冲区中读取数据,缓冲区没有数据可读取将会发生阻塞等待。

  • 相关阅读:
    面试笔试题
    类型转换
    c++11之智能指针
    c++预处理命令
    java的javac不能正常运行
    状态模式
    观察者模式Observer
    带图形界面的虚拟机安装+Hadoop
    测试工具的使用:JUnit、PICT、AllPairs
    Test_1 一元二次方程用例测试以及测试用例
  • 原文地址:https://www.cnblogs.com/k5210202/p/13071372.html
Copyright © 2011-2022 走看看