TCP编程入门

TCP编程 - C/S

Server端

import socket
import logging
import threading
import time

logging.basicConfig(format='%(thread)s %(threadName)s %(message)s',level=logging.INFO,)

# 两次绑定同一个监听端口会造成端口冲突,抛OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
s = socket.socket()     #  缺省TCP协议,IP v4adressd

addr = ('127.0.0.1',9999)     #  二元组,IP v4为字符串,Port为数值,65535
# '127.0.0.1'本地回环地址,当前主机设置成回环地址,其他主机无法访问当前主机,因为其他主机设置成回环地址,只能在自己的主机内一直回环
# 当前主机使用回环地址,只要是当前操作系统的资源,端口对应就可以访问,且不需要路由

s.bind(addr)                #  绑定addr
s.listen()                  #  server端开始监听
data = s.accept()           #  阻塞等待Client端建立连接,用于获取和Client传送数据的Socket对象

s_connect,c_info = data
logging.info((type(s_connect),s_connect))    #  laddr(local addr,本地地址);raddr(remote addr,远程地址,也称对端地址)
logging.info((type(c_info),c_info))
c_data = s_connect.recv(1024)     #   接收Client端消息,阻塞,设置缓冲区大小,1024字节
print(c_data)
s_connect.send(c_data)            #   向Client端发送消息,send方法也有缓冲区,如果缓冲区满了,也会阻塞

s_connect.close()    #  连接Client端通信的socket对象,也需要归还
s.close()        #   socket占用文件描述符,一定要归还资源

聊天软件

初版

  • 实现多个Client端和Sever端的通信,且故不干扰
    • 主线程的socket对象,主要负责监听
    • accept线程,无限循环与Client端建立连接
    • 为每一个建立连接的Client创建进行,互不干扰的和Sever通信

注意:每次accept,就会生成一个socket对象与Client端进行连接通信,和主线程的socket不是一个对象

import logging
import threading
import socket
import datetime
import time

logging.basicConfig(format='%(asctime)s %(thread)d : %(message)s',level=logging.INFO)


class ChatServer:
    def __init__(self,ip,port):  #  启动聊天服务
        self._addr = ip,port
        self._server = socket.socket()
        self.clients = {}        #  存放客户端信息

    def start(self):    # 启动监听
        self._server.bind(self._addr)
        self._server.listen()
        threading.Thread(target=self.s_accept,name='accept').start()   #  创建一个线程,无限循环与Client端建立连接

    def s_accept(self):   # 无限连接线程
        while True:
            s_conn,c_addr = self._server.accept()       # 与Client端建立链接阻塞主进程,所以创建一个线程
            self.clients[c_addr] = s_conn
            threading.Thread(target=self.s_recv,args=(s_conn,c_addr)).start()    #  建立一个连接,就创建一个线程专门为Client端服务

    def s_recv(self,sock:socket.socket,client):    #  接收客户端数据
        while True:       #  每个线程中的Client端,可以循环发送和接收信息,直至退出
            c_info = sock.recv(1024)    #  多个Client端的时候,阻塞主线程,为每一个Client端创建一个线程
            time.sleep(1)
            msg = '{:%Y/%m/%d %H:%M:%S} {}:{} {}'.format(datetime.datetime.now(),*client,c_info).encode()
            sock.send(msg)

    def stop(self):    #  停止服务
        for s in self.clients.values():
            s.close()
        self._server.close()

c_s = ChatServer(ip='127.0.0.1',port=9999)
c_s.start()
改进版
  • 遍历字典实现多个用户群聊
    • 使用字典遍历,给每一个Client端发送信息
    • Client主动退出的时候,会发一个b’’,且抛异常
      • 根据这个特性,当c_inof为b’‘或者b’quit’,退出循环
    • 聊天服务退出的时候,抛异常需要捕获记录且打印
import logging
import threading
import socket
import datetime

logging.basicConfig(format='%(asctime)s %(thread)d : %(message)s',level=logging.INFO)


class ChatServer:
    def __init__(self,ip,port):  #  启动聊天服务
        self._addr = ip,port
        self._server = socket.socket()
        self.clients = {}        #  存放客户端信息
        self.event = threading.Event()    #  线程同步

    def start(self):    # 启动监听
        self._server.bind(self._addr)
        self._server.listen()
        threading.Thread(target=self.s_accept,name='accept').start()

    def s_accept(self):   # 无限连接线程
        while not self.event.is_set():     # 主线程退出,self.event置1,循环终止
            try:
                s_conn,c_addr = self._server.accept()      #  注意,s_conn如果设置为self.s_conn,会出现死循环
            except Exception as e:      #  当self._server关闭,抛异常,捕获记录且终止循环
                logging.info(e)
                break

            self.clients[c_addr] = s_conn
            threading.Thread(target=self.s_recv,args=(s_conn,c_addr)).start()

    def s_recv(self,sock:socket.socket,client):    #  接收客户端数据
        while not self.event.is_set():     #  如果使用self.conn,停止了刚创建的连接,而自己的没停止,造成死循环
            c_info = sock.recv(1024)    #  如果Client端主动断开,此时sock连接断开,抛异常
            if c_info == b'' or c_info == b'quit':
                break
            logging.info(('c_info is ',c_info))
            msg = '{:%Y/%m/%d %H:%M:%S} {}:{} {}'.format(datetime.datetime.now(),*client,c_info).encode()

            for c in self.clients.values():    # 遍历,实现多个Client端互通
                c.send(msg)

    def stop(self):    #  停止服务
        self.event.set()     #  停止服务,线程时间置1,s_accept和s_recv线程循环退出
        for s in self.clients.values():
            s.close()
        self._server.close()


c_s = ChatServer(ip='127.0.0.1',port=9999)
c_s.start()

while True:
    cmd = input('>>>>>>').lower().strip()
    if cmd == 'quit':
        c_s.stop()
        break
    print(threading.enumerate())

print(threading.enumerate())

线程安全
  • CPtyhon中,GIL保证了python的读写操作为原子操作
    • 单独操作字典的某一项item是安全的
    • 但是遍历过程是不安全的,可以被时间片打断
    • 如果字典正在遍历,其他线程尝试修改字典的SIZE,就会崩掉正在遍历字典的线程
  • 加锁,虽然解决了线程安全问题,但是当字典Client较多的时候,这不是一个好的选择
import logging
import threading
import socket
import datetime

logging.basicConfig(format='%(asctime)s %(thread)d : %(message)s',level=logging.INFO)


class ChatServer:
    def __init__(self,ip,port):  #  启动聊天服务
        self._addr = ip,port
        self._server = socket.socket()
        self.clients = {}        #  存放客户端信息
        self.event = threading.Event()    #  线程同步
        self.lock = threading.Lock()

    def start(self):    # 启动监听
        self._server.bind(self._addr)
        self._server.listen()
        threading.Thread(target=self.s_accept,name='accept').start()

    def s_accept(self):   # 无限连接线程
        while not self.event.is_set():     # 主线程退出,self.event置1,循环终止
            try:
                s_conn,c_addr = self._server.accept()      #  注意,s_conn如果设置为self.s_conn,当某个Client端退出,会终止self.s_conn,且死循环
            except Exception as e:      #  当self._server关闭,抛异常,捕获记录且终止循环
                logging.info(e)
                break

            with self.lock:     # 确保字典增加的时候,其他线程不会遍历
                self.clients[c_addr] = s_conn
            threading.Thread(target=self.s_recv,args=(s_conn,c_addr)).start()

    def s_recv(self,sock:socket.socket,client):    #  接收客户端数据
        while not self.event.is_set():     #  如果使用self.conn,造成死循环
            c_info = sock.recv(1024)    #  如果Client端主动断开,此时sock连接断开,抛异常
            
            if c_info == b'' or c_info == b'quit': 
                with self.lock:         #  确保字典的删除
                    self.clients.pop(client)     #  Client退出,并退出
                    sock.close()    #   与其通信的socket对象关闭
                    break

            logging.info(('c_info is ',c_info))
            msg = '{:%Y/%m/%d %H:%M:%S} {}:{} {}'.format(datetime.datetime.now(),*client,c_info).encode()
            with self.lock:          # 确保字典遍历完成,但是字典越大,效率越高
                for c in self.clients.values():    # 遍历,实现多个Client端互通
                    c.send(msg)

    def stop(self):    #  停止服务
        self.event.set()     #  停止服务,线程时间置1,s_accept和s_recv线程循环退出
        with self.lock:
            for s in self.clients.values():   # 确保字典遍历
                s.close()
        self._server.close()

c_s = ChatServer(ip='127.0.0.1',port=9999)
c_s.start()

while True:
    cmd = input('>>>>>>').lower().strip()
    if cmd == 'quit':
        c_s.stop()
        break
    print(threading.enumerate())    # 测试用

print(threading.enumerate())
尝试

此处存疑,线程退出会进入死循环,暂时未找到原因

import logging
import threading
import socket

logging.basicConfig(format='%(asctime)s <%(thread)d %(threadName)s>, %(message)s',level=logging.INFO,
                    datefmt='%Y/%m/%d %H:%M:%S')

class ChatServer:           # Server端
    def __init__(self,ip='127.0.0.1',port=9999):   #  Server端需要ip地址和port端口
        self.addr = ip,port
        self._server = socket.socket()       #  创建好server端的socket对象,不着急监听;TCP协议,IP v4地址
        self.event = threading.Event()       #  用于进程间同步
        self.s_pool = {}                     #  使用字典,key为Client的addr,value为Server端专门连接Client端的socket对象
        self.lock = threading.Lock()

    def start(self):
        self._server.bind(self.addr)
        self._server.listen()              # 主线程的Server端socket对象,只用来监听
        threading.Thread(target=self.s_accept,name='s_listen',daemon=True).start()    # accept阻塞主线程,开启工作线程专门用来接收Client的连接请求
                                                                                      # deamon线程,可以不用设置,服务器退出,监听线程退出
    def s_accept(self):
        while not self.event.is_set():
            try:
                self.s_connect,self.c_addr = self._server.accept()   #  rece接收Client端的信息,会阻塞
            except Exception as e:
                logging.info(e)        #  如果服务器关闭,self._server不存在,导致赋值语句失败,捕获异常并记录
                break

            with self.lock:
                self.s_pool[self.c_addr] = self.s_connect          # 字典的增加
            threading.Thread(target=self.s_recv,args=(self.s_connect,self.c_addr)).start()   #  每连接一个Client端,创建一个工作线程,为每个Client端服务

    def s_recv(self,connect,addr):
        while not self.event.is_set():       # 线程同步,self.event置1,退出循环
            c_data = connect.recv(1024)

            with self.lock:
                if c_data == b'' or c_data == b'quit':
                    self.s_pool.pop(addr)        # 字典的减少
                    break
                logging.info(c_data)

                # CPython解释器中,GIL保证了多线程中的内建数据结构的读写操作是原子性的,
                # 但是,循环遍历不是原子操作,有可能遍历的时候,其他线程在修改字典的大小,且字典遍历的时候,不能改变size,然后崩掉Client端的线程
                for c in self.s_pool.values():   # 遍历字典,给每一个Client端发送信息
                    c.send(c_data)

    def stop(self):
        with self.lock:
            for s in self.s_pool:         # 字典的遍历
                s.close()                 # 关闭每一个和Cilent端连接的socket对象
        self.event.set()              # 同步事件,置1
        self._server.close()          # 关闭服务器
        pass


chat_s = ChatServer()
chat_s.start()
print('~~~~~~~~')
while True:
    s_stop = input('>>>>>>>').lower().strip()
    if s_stop == 'quit':
        threading.Event().wait(3)
        print('clear is over')
        chat_s.stop()
        break
    print(threading.enumerate())

print(threading.enumerate())

测试多线程中,字典的遍历、字典size的变化(减少、增加)、字典值修改的安全性

  • 创建两个deamon工作线程
    • 一个线程无限循环向字典中增加元素
    • 一个线程无限循环遍历字典且同时修改元素的值
  • 结论
    • 字典的每次读 / 写都是原子操作,循环不是原子操作
    • 多线程中,使用字典增删遍历,是线程不安全的,需要锁
    • 但是上锁,其他线程会一直阻塞,当字典中元素过多,遍历时间较长的时候,不是一个好选择
import logging
import time
import threading
import random

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

target = {}

def additem(d:dict):
    count = 1
    while True:
        time.sleep(0.1)
        d[count] = random.randrange(1,101)
        count += 1

def iterdict(d:dict):
    while True:
        for k,v in d.items():
            time.sleep(0.1)
            logging.info((k,v))
            d[k] = random.randint(1,100)

# 使用daemon线程,主线程退出,kill所有daemon线程
threading.Thread(target=additem,args=(target,),name='add_thread',daemon=True).start()
threading.Thread(target=iterdict,args=(target,),name='iter_thread',daemon=True).start()

while True:
    time.sleep(1)
    print(threading.enumerate())   #  每隔一秒,打印一次,正常3个线程

    if threading.active_count() <= 2:   #  字典遍历的时候,不能修改size,崩掉遍历的线程
        keys = list(target.keys())
        print('=====end=====')
        print(keys)
        break

Client端

  • Client端不需要bind(),不需要listen(),不需要accept()
    • 只需要创建socket对象之后connect()
    • 需要两个线程,主线程用来send()信息;工作线程一直recv(),阻塞等待Server端的回复。
import socket
import threading
import logging
import datetime

logging.basicConfig(format='%(asctime)s -- %(thread)d : %(message)s',level=logging.INFO,datefmt='%Y/%m/%d %H:%M:%S')

class Client:
    def __init__(self,ip='0.0.0.0',port=9999):
        self._addr = (ip,port)
        self._client = socket.socket()
        self.event = threading.Event()

    def start(self):
        self._client.connect(self._addr)       #  注意:Client端不需要bind(),不需要listen(),不需要accept()
        threading.Thread(target=self.recv,daemon=True).start()   #  主线程退出,工作线程也退出

    def send(self,info):
        self._client.send(info.encode())

    def recv(self):
        while not self.event.is_set():
            msg = '{:%Y/%m/%d %H:%M:%S} {} {}'.format(datetime.datetime.now(),*self._addr)
            data = self._client.recv(1024)
            logging.info(data)

    def stop(self):
        self._client.close()

def main():
    c = Client('127.0.0.1')
    c.start()
    while True:
        cmd = input('>>>>>')
        if cmd.lower().strip() == 'quit':
            c.stop()
            break
        c.send(cmd)

if __name__ == '__main__':
    main()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值