Python网络编程底层包socket的使用

文章目录

  • 构建socket对象
  • TCP编程
    • TCP服务端编程
    • TCP客户端
  • UDP编程
    • UDP服务端
    • UDP客户端
    • 心跳机制
  • UDP协议和TCP协议的区别

Python中提供socket.py标准库,非常底层的接口库。
Socket是一种通用的网络编程接口,和网络层次没有一一对应的关系。

构建socket对象

socket = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
  • family : 协议族
名称 含义
AF_INEET IPV4(默认值)
AF_INET6 IPV6
AF_UNIX Unix Domain Socket, windows没有
  • type: socket类型
名称 含义
SOCK_STREAM 面向连接的流套接字, 即TCP协议(默认值)
SOCKET_DGRAM 无连接的数据报文套接字。即UDP协议

TCP编程

socket编程需要两端,一般来说,需要一个服务端(Server),一个客户端(Client)

这种编程模式也称为CS编程

socket对象常用方法:

方法 含义
socket.getpeername 返回套接字的远程地址,返回值是raddr
socket.getsocketname 返回套接字自己的地址,返回值为laddr

TCP服务端编程

  • 创建socket对象
  • 使用bind方法,绑定IP地址,Address和端口Port
  • 使用listen开始监听,在上面以绑定的地址上
  • 使用accept开始等待连接进来,获取用于传送数据的socket对象和addr。注意使用accept会发生阻塞,惯用放在新的线程里面
    • 接收数据recv, buffsize可以选用1024,接收的字节流bytes
    • 发送数据send,发送的也是字节流bytes

构建一个TCP协议的远古版本群聊服务端

import datetime
import socket
import threading


class ChatSocket:
   def __init__(self, ip="127.0.0.1", port=9999): # 本地localhost也可以用""或者"localhost"表示
       self.addr = (ip, port)   
       self.socket = socket.socket()  # 创建socket对象
       self.clients = {}  # 创建客户端词典
       self.event = threading.Event()
       self.lock = threading.Lock() # 设置一把锁,为了遍历客户端的字典的时候保证线程安全

   def start(self):
       self.socket.bind(self.addr)  # 绑定端口
       self.socket.listen(2)  # 开始监听,监听客户端无上限
       threading.Thread(target=self.accept).start()  # 如果不创建此线程,下面的等待连接accept会阻塞当前的主线程,导致无法执行最后的stop

   def accept(self):
       while not self.event.is_set():  # 标志位的设置是为了stop时,不再 等待新线程的接入
           new_socket, client_info = self.socket.accept()  # 等待连接,阻塞,多人连接
           with self.lock:
               self.clients[client_info] = new_socket
           print(client_info, "已连接")
           t = threading.Thread(target=self.recv_send, args=(new_socket, client_info)) # 由于recv会发生阻塞行为
           t.start()

   def recv_send(self, new_socket, clint_info):
       while not self.event.is_set():
           data = new_socket.recv(1024)  # 设置一次最大读取数据上限是1024字节,阻塞,如果客户端主动断开连接,会收到空的字符串
           print(data)
           if data.decode().strip() == "quit" or data.decode().strip() == "":  # 客户端上的退出按钮会返回一个空字符串或者发送quit退出
               print("用户", clint_info[1], "quit")
               with self.lock:
                   self.clients.pop(clint_info)
                   new_socket.close()
               break
           msg = "用户{} {}说: {}".format(clint_info[1], datetime.datetime.now(), data.decode(encoding="gbk"))
           print(msg)
           with self.lock:
               for s in self.clients.values():  # 群聊的性质就是将从client接收的信息,转发给当前所有连接到server的client客户端,是一种很,标识符的选择一定要慎重
                   s.send(msg.encode(encoding="gbk"))

   def stop(self):
       self.event.set()  # 设为True
       with self.lock:
           for s in self.clients.values():
               s.close()
       self.socket.close()


cs = ChatSocket()
cs.start()
while True:
   cmd = input(">>").strip()
   if cmd == "quit":
       cs.stop()
       threading.Event().wait(3)
       break

TCP客户端

再构建一个客户端
客户端连接步骤:

  • 创建Socket对象
  • 连接到远端服务端的IP和port端口, connect方法
  • 传输数据
    • send发送数据
    • recv接收数据, 会阻塞
  • 关闭连接,释放资源
import socket
import threading
import logging

fmt = "%(asctime)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(format=fmt, level=logging.INFO)

class Client:
    def __int__(self, ip="127.0.0.1", port=9999):
        self.sock = socket.socket() # 建立socket对象
        self.addr = ip, port
        self.event = threading.Event()

    def start(self):
        self.sock.connect(self.addr)
        self.sock.send("i'm read")
        threading.Thread(target=self.recv, name="recv").start()

    def recv(self):
        while not self.event.is_set():
            try:
                data = self.sock.recv(1024)
            except Exception as e:
                logging.error(e)
                break

    def send(self, msg:str):
        data = "{}\n".format(msg.strip().encode())
        self.sock.send(data)

    def stop(self):
        self.sock.close()
        self.event.wait(3)
        self.event.set()
        logging.info("Client stop..")


def main():
    cc = Client()
    cc.start()
    while True:
        cmd = input(">>input msg").strip()
        if cmd == "quit":
            cc.stop()
            break
        cc.send(cmd)
        
        
if __name__ == "__main__":
    main()

UDP编程

UDP编程和上面的TCP编程有些不同,主要体现在协议本身的不同

UDP是无连接协议,所以可以只有任何一端,例如客户端数据发往服务端,服务端存在与否无所谓。

注意
UDP的socket多想创建后,是没有占用本地址和端口的

UDP协议的常见方法:

方法 说明
bind 可以指定地址和端口,会立即占用
connect 可以立即占用本地地址和端口laddr,填充远端地址和端口raddr
sendto 可以立即占用本地地址和端口laddr,并把数据发往指定远端,只要有了本地地址和端口,sendto就可以像任何远端发送数据
send 需要和connect配合,可以使用已经从本地端口把数据发送raddr指定的远端
recv 要求一定占用了本地端口后,返回接收的数据
recvfrom 要求一定占用了本地端口后,返回接收的数据和对端地址的二元组

UDP服务端

  • 创建socket对象,type参数为SOCKET_DGRAM无连接的数据报
  • 绑定IP和端口
  • 传输数据
    • 接收数据,socket.recvfrom(1024),返回一个二元组(data, address)
    • 发送数据,socket.sendto(data, address),发送给某地址某信息
  • 释放资源
# echoserver模型的聊天室服务端
import threading
import socket
import time


class ChatServerUDP:
    def __init__(self, ip="127.0.0.1", port=9999, deadtime=10):
        self.addr = ip,port
        self.sock = socket.socket(type=socket.SOCK_DGRAM)
        self.event = threading.Event()
        self.clients = {}
        self.deadtime = deadtime

    def start(self):
        self.sock.bind(self.addr)
        threading.Thread(target=self.recv).start()

    def recv(self):
        while not self.event.is_set():
            deadclient = set()  # 用来处理死了的客户端

            data, raddr = self.sock.recvfrom(1024)

            if data == b"==heatbeat==":  # 如果接收到心跳包,说明客户端还活着
                current = time.time()
                self.clients[raddr] = current
                continue

            if data == b"quit": # 注意这里,是bytes字节流,千万别搞错
                self.clients.pop(raddr)
                continue
            current = time.time()
            self.clients[raddr] = current
            print(data.decode())
            msg = "hello"

            for c, stamp in self.clients.items():
                if time.time() - stamp < self.deadtime:  # 这里是用10秒表示过期时长, 可以自己调控
                    self.sock.sendto(msg.encode(), c)  # 只向没有过期的client的发送data
                else:
                    deadclient.add(c)

            for c in deadclient:  # 将过期的client清除掉
                self.clients.pop(c)


    def stop(self):
        self.event.set()
        self.sock.close()

def main():
    c = ChatServerUDP()
    c.start()
    while True:
        cmd = input(">>请输入:").strip()
        if cmd == "quit":
            c.stop()
            break

if __name__ == '__main__':
    main()

UDP客户端

import threading
import socket
import logging

fmt = "%(asctime)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(format=fmt, level=logging.INFO)


class ChatClientUDP:
    def __init__(self,ip, port):
        self.addr = ip, port
        self.sock = socket.socket(type=socket.SOCK_DGRAM) # 选用UDP协议
        self.event = threading.Event()

    def start(self):
        self.sock.connect(self.addr)
        threading.Thread(target=self.recv).start()
        threading.Thread(target=self.hb).start()

    def hb(self):  # 心跳包发送
        while not self.event.wait(5):  # 客户端没关闭,如果event一直是false的情况下每五秒返回一个false
            self.send("==heatbeat==")

    def recv(self):
        while not self.event.is_set():
            data, raddr = self.sock.recvfrom(1024)  # 工作线程只负责就收信息
            print("他说:",data.decode())

    def send(self, msg:str):
        self.sock.send(msg.encode())

    def stop(self):
        self.event.set()
        self.send("quit")
        self.sock.close()

def main():
    c1 = ChatClientUDP("127.0.0.1", 6000)
    c2 = ChatClientUDP("127.0.0.1", 6000)
    c1.start()
    c2.start()
    while True:
        cmd = input(">>请说:").strip()
        if cmd == "quit":
            c1.stop()
            c2.stop()
            break
        c1.send(cmd)   # 这里主线程用来信息
        c2.send(cmd)


if __name__ == "__main__":
    main()

心跳机制

1.一般来说,客户端定时发往服务端的,服务端并不要需要返回ack恢复给客户端,只需要记录该客户端或者就行了。
2.如果是服务端定时发往客户端的,一般需要客户端ack响应来表示或者,如果没收收到客户端的ack, 服务端就移除其信息。这种实现较为复杂,用的较少。
3.也可以双向发送心跳的。用的较少

上面的UDP的服务端和客户端使用了心跳机制,如果一个客户端或者就会按照指定的间隔发送心跳包,服务端每收到心跳包,就更新这个客户端的当前在线时间, 超时过期机制就会把超过指定间隔时间的客户端清理掉

UDP协议和TCP协议的区别

区别 TCP UDP
连接类型 面向连接 无连接
可靠性 可靠 不可靠
数据包是否有序 数据包有序 数据包无序
使用场景 大多数需要数据可靠的场合 视频、音频等不需要数据可靠的地方

连接

  • TCP需要通讯双方预先建立连接,需要三次握手,四次断开
  • UDP不需要预先建立连接

可靠性

  • TCP需要确定每一个包是否收到,丢包重发,效率低一些
  • UDP尽最大努力交付数据,不需要确认数据包,丢包无法知道,也不重复,效率高一些

有序

  • TCP包有序号,可以进行顺序控制,第一个包序号随机生成,之后的序号和它有关
  • UDP包无序,无法纠正,只能在应用层进行验证

你可能感兴趣的:(Python核心部分)