python socket网络编程日记

本文详细介绍了Python中使用socket进行网络通信的过程,包括TCP和UDP协议的特点,以及socket的初始化、服务器端和客户端代码实现。此外,还展示了如何处理远程执行命令、文件上传和并发聊天的场景,涉及到数据接收的完整性和粘包问题的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 网络通信三要素

    1. IP地址
    2. 端口号
    3. 协议
      1. UDP:
        User Datagram Protocal 用户数据报协议
        1. 面向无连接:传输数据之前源端和目的端不需要建立连接。
        2. 每个数据报代销限制在64K以内。
        3. 面向报文的不可靠协议。
        4. 传输速率快,效率高。
        5. 现实生活实例:邮局寄信、实时聊天、视频会议。
      2. TCP:
        Transmission Control Protocal 传输控制协议
        1. 面向连接:出书数据之前需要建立连接。
        2. 在连接过程中进行大量数据传输。
        3. 通过“三次握手”的方式完成连接,是安全可靠的协议。
        4. 传输速度慢,效率低。
  2. 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()
    
      #套接字的文件描述符

     

  3. 简单通信实例:

    1. 描述:
      客户端连接服务器端与服务器进行通信。

      SOCK_STREAM:TCP
      SOCK_Dgram :UDP

      family=AF_INET:服务器之间的通信
      family=AF_INET6: ipv6
      family=AF_UNIX:unix不同进程间通信

    2. 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
      

       
    3. 服务器端代码
      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()

       

    4. 客户端代码
      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()

       

    5. 执行结果:
      1. 启动服务器端:
      2. 启动客户端:
        1. 服务器端
        2. 客户端
      3. 客户端输入内容,服务器端接收数据。

      4. 再启动一个客户端:
        再启动一个客户端,这个新启动的客户端并不能与服务器通信,因为服务器正在与第一个客户端连接,此时再有客户端连接服务器,就需要排队,第一个客户端关闭后,第二个客户端才能与服务器建立连接通信。
  4. 远程执行命令

    1. 描述:
      客户端发送命令,服务器端接收命令后执行,将执行结果返回给客户端。
      1. 使用subprocess模块执行客户端发送过来的命令。
      2. obj = subprocess.Popen(str(data, 'utf8'), shell=True, stdout=subprocess.PIPE)
        使用subprocess.Popen()来执行命令。第一个参数为命令字符串,第二个参数为是否为shell命令,第三个参数为将返回结果输出到哪里,我们输出到PIPE管道里。
      3. cmd_ret = obj.stdout.read() 从标准输出中读取执行结果。
      4. 最后将结果cmd_ret发送给客户端。
      5. 客户端在接收数据的时候一开始只能接收1024字节的数据,如果服务器端发送超过1024字节的数据,客户端将接收不到大于1024字节的数据,就会产生错误的结果,针对这一问题的解决:
        1. 客户端要循环接收数据,每次接收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. 第2个问题是粘包的问题。
          服务器端向客户端先发送大小数据,紧接着就发送具体内容数据,两次发送距离时间很短,两个数据包可能会粘在一起,这就是粘包现象。

                 
          conn.send(ret_len) # 发送长度数据
          conn.sendall(cmd_ret) # 发送返回结果数据
          

          针对这个问题的解决方法是:服务器端发送完长度数据后,客户端给服务器端一个反馈,服务器端得到反馈,知道客户端已经接收了长度数据,再向客户端发送内容数据。

    2. 服务器端:
      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()
      

       

    3. 客户端:
      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()

       

    4. 执行结果
      1. 启动服务器端
      2. 启动客户端
      3. 客户端输入dir命令(返回字节不超过1024)
      4. 客户端输入ipconfig/all命令(返回字节超过1024)

        可以看出已经可以显示出全部的结果。
  5. 向服务器上传文件

    1. 描述
      在客户端中输入"post|文件路径"可以将文件上传至服务器uploads目录中。例如:要上传的文件为a.txt,那么输入命令:post|a.txt
      我了简单期间,我们将文件放在目录下,目录结构如下:
      1. 客户端:
        1. 分解命令'post|***.jpg'
        2. 获取文件绝对路径
        3. 获取文件大小
        4. 将文件名、文件大小数据发送给服务器端
          # 获取目录的绝对路径
          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')) # 将文件信息先发送给服务器端

           

        5. 以二进制读‘rb’打开文件
        6. 循环发送文件,直至发送完毕
          循环终止的条件为:已发送数据数量=文件大小
      2. 服务器端:
        1. 服务器先接收到客户端发送的文件信息
        2. 在服务器指定位置以写二进制方式打开文件
        3. 循环接收二进制,并在服务器端写入文件
        4. 写入文件成功,发送上传成功信息给客户端
    2. 服务器端
      # _*_ 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()

       
    3. 客户端
      # _*_ 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()

       

    4. 执行结果



  6. 并发聊天

    1. 描述
      服务器端可以同时接收多个客户端建立通讯连接。
      需要是使用:
      socketserver模块
      
    2. SocketServer模块:
      1. SocketServer模块简化了编写网络服务程序的任务,同时SocketServer模块也是python标准库中很多服务器框架的基础。
        socketserver模块可以简化服务器的编写,python把网络服务抽象成两个主要的类,一个是Server类,用于处理连接相关的网络操作;另一个是RequestHandler类,用于处理数据相关的操作。并且提供两个MixIn类,用于扩展Server,实现多进程、多线程。
      2. Server类:
        包含5种Server类。BaseServer(不直接对外服务)。TCPServer使用TCP协议,UDPServer使用UDP协议,还有两个不常使用的UnixStreamServer和UnixDatagramServer,这两个类仅仅在unix环境下有用(AF_unix)。
      3. 创建一个socketserver至少有以下几步:
        1. 创建request handler类,必须继承BaseRequestHandler类,并且必须重写handler()方法,并将业务逻辑写在handler()方法里。
          class MyServer(socketserver.BaseRequestHandler):
          
              def handle(self):
                  pass

           

        2. 必须通过服务器地址和request handler类实例化一个server类。
           
          server = socketserver.ThreadingTCPServer(('127.0.0.1', 8000), MyServer)
          

           

        3. 然后调用服务器对象的handler_request()或者serve_forever()方法处理一个或多个请求。
          server.serve_forever()

           

        4. 最后,调用server_close()关闭socket。
    3. 客户端
      客户端不需要修改
    4. 服务器端
      服务器端需要实现并发
      # _*_ 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()

       

    5. 执行结果:
    6.  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值