网络编程之TCP编程(socket、服务端、客户端)

1. socket介绍

1.1 socket套接字

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

1.2 协议族

AF表示Address Family,用于socket()第一个参数
协议族

1.3 Socket类型

socket类型

2. TCP编程

  socket编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server,客户端称为Client。这种编程模式也称为CS编程。

2.1 TCP服务端编程

2.1.1 服务端编程步骤

  • 创建socket对象
  • 绑定IP地址Address和端口Port。bind()方法,IPv4地址为一个二元组(‘IP地址字符串’,Port)
  • 开始监听,将在指定的IP的端口上监听,listen()方法。
  • 获取用于传送数据的socket对象。socket.accept() -> (socket object, address info) accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元地址是远程客户端的地址,IPv4中它是一个二元组(clientaddr, port)
  • 接收数据:recv(bufsize[, flags]) 使用缓冲区接收数据
  • 发送数据:send(bytes)发送数据

TCP编程步骤TCP server端开发:

# TCP server 端开发
import socket
# import time


server = socket.socket()  # TCP 连接 IPv4
ip = '127.0.0.1'  # 本机回环地址,永远指向本机
port = 9999  # 建议使用1000以上; TCP 65536种状态
server.bind((ip, port))  # address,此方法只能绑定一次
server.listen()  # 真正的显示出端口,监听不是阻塞函数
# time.sleep(100)
print(server)
# print(server.accept())  # 默认阻塞,不懂千万不要修改

new_socket, client_info = server.accept()
print(new_socket)
print('new_socket', client_info)
while True:
    # new_socket.send(b'server ack')
    data = new_socket.recv(1024)  # 缺省情况下是阻塞的

    print(data)
    new_socket.send('server ack. data={}'.format(data.decode()))

new_socket.close()

# new2, client2 = server.accept()  # 新的连接,之前的连接已经关闭,并且两次连接可能在不同的进程
# print(new2)
# print('new2', client2)
# data = new2.recv(1024)
# print(data)
# new2.send('server new2 ack. data={}'.format(data.encode()))
# new2.close()

server.close()

实战——写一个群聊程序
需求分析
聊天工具是CS程序,C是每一个客户端client,S是服务器端server
服务器应该具有的功能:

  1. 启动服务,包括绑定地址和端口,并监听
  2. 建立连接,能和多个客户端建立连接
  3. 接收不同用户的信息
  4. 分发,将接收的 某个用户的信息转发到已连接的所有客户端
  5. 停止服务
  6. 记录连接的客户端

代码实现

# tcp server 端开发

import socket
import threading
import logging
from datetime import datetime


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


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999):
        self.sock = socket.socket()
        self.address = ip, port
        self.event = threading.Event()
        self.lock = threading.Lock()
        self.clients = {}

    def start(self):
        self.sock.bind(self.address)
        self.sock.listen()

        threading.Thread(target=self.accept, name='accept').start()

    def accept(self):
        while not self.event.is_set():
            try:
                new_sock, client_info = self.sock.accept()  # 阻塞等待连接
            except Exception as e:
                logging.error(e)

            with self.lock:
                self.clients[client_info] = new_sock
                threading.Thread(target=self.rec, name='rec', args=(new_sock, client_info)).start()

    def rec(self, sock, client):
        while not self.event.is_set():
            try:
                data = sock.recv(1024)  # 阻塞等待接收信息,接收信息也可能出现异常
            except Exception as e:
                logging.error(e)
                data = b''
            print(data.decode(encoding='cp936'))

            if data.strip() == b'quit' or data.strip() == b'':  # 客户端主动断开连接
                with self.lock:
                    self.clients.pop(client)  # 将断开连接的客户ip和端口从字典中移除,因为下文还要遍历字典
                sock.close()  # 此句比较耗时,可以放在锁的外面
                break

            msg = "{:%Y/%m/%d %H:%M:%S} [{}:{}] - {}".format(datetime.now(), *client, data.decode(encoding='cp936'))

            exc = []
            exs = []
            with self.lock:
                for c, s in self.clients.items():  # 遍历的是时候不允许别人pop和add,所以加锁
                    try:
                        s.send(msg.encode(encoding='cp936'))  # 给所有的new_sock发送的信息。注意此句可能会出现异常,如网络断了
                    except Exception as e:
                        logging.error(e)
                        exc.append(c)
                        exs.append(s)
                for c in exc:
                    self.clients.pop(c)
            for s in exs:  # 此句比较耗时,所以放在锁外面
                s.close()

    def stop(self):
        self.event.set()
        with self.lock:
            values = list(self.clients.values())
            self.clients.clear()  # 这是一个好习惯
        for s in values:
            s.close()
        self.sock.close()


cs = ChatServer()
cs.start()

while True:
    cmd = input(">>>").strip()
    if cmd == 'quit':
        cs.stop()
        break
    logging.info(threading.enumerate())
    logging.info(cs.clients)
    

socket常用方法

makefile
socket.makefile(mode=‘r’, buffering=None, *, encoding=None, error=None, newline=None)
创建一个与该套接字相关联的文件对象,将recv方法看做读方法,将send方法看做写方法。

import socket


s = socket.socket()
s.bind(('127.0.0.1', 9999))
s.listen()
s1, info = s.accept()
print(s1.getpeername())  # ('127.0.0.1', 55934)
print(s1.getsockname())  # ('127.0.0.1', 9999)

f = s1.makefile('rw')
data = f.read(10)  # 一次读取10个字节
print(data)
msg = 'server rec {}'.format(data)
f.write(msg)
f.flush()
print(f, s1, sep='\n')

f.close()
s1.close()
s.close()

makefile练习
使用makefile改写群聊类

import datetime
import threading
import socket
import logging


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


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999):
        self.sock = socket.socket()
        self.address = ip, port
        self.event = threading.Event()
        self.clients = {}
        self.lock = threading.Lock()

    def start(self):
        self.sock.bind(self.address)
        self.sock.listen()
        threading.Thread(target=self.accept, name='accept').start()

    def accept(self):
        while not self.event.is_set():
            new_sock, client_info = self.sock.accept()

            new_file = new_sock.makefile('rw')
            with self.lock:
                self.clients[client_info] = new_file, new_sock  # 增加了item,所以必须加锁,多线程处理同一个资源
                threading.Thread(target=self.rec, name='rec', args=(new_file, client_info)).start()

    def rec(self, f, client):
        while not self.event.is_set():
            line = f.readline()  # 阻塞等一行来,输入数据的时候要加换行符
            print(line)
            if line.strip() == 'quit' or line.strip() == '':  # line为字符串。不能再写成b''和b'quit'了
                with self.lock:
                    _, s = self.clients.pop(client)
                f.close()
                s.close()
                break

            msg = '{:%Y/%m/%d %H:%M:%S} [{}: {}] {}'.format(datetime.datetime.now(), *client, line)
            # 此处的line不用解码了,因为readline读取的是字符串

            with self.lock:
                for ff, _ in self.clients.values():  # 注意ff不能与上面的f重复
                    ff.write(msg)
                    ff.flush()

    # def rec(self, f, client):
    #     while not self.event.is_set():
    #         try:
    #             line = f.readline()
    #         except Exception as e:
    #             logging.error(e)
    #             line = 'quit'
    #         msg = line.strip()
    #         if msg == 'quit' or msg == '':
    #             with self.lock:
    #                 _, sock = self.clients.pop(client)
    #                 f.close()
    #                 sock.close()
    #                 logging.info('{} quits.'.format(client))
    #                 break
    #         msg = '{:%Y/%m/%d %H:%M:%S} [{}: {}] {}'.format(datetime.datetime.now(), *client, line)
    #         logging.info(msg)
    #         with self.lock:
    #             for ff, _ in self.clients.values():
    #                 ff.write()
    #                 ff.flush()

    def stop(self):
        self.event.set()
        # keys = []
        with self.lock:
            for f, s in self.clients.values():
                f.close()
                s.close()

        self.sock.close()


def main():
    cs = ChatServer()
    cs.start()

    while True:
        cmd = input(">>>").strip()
        if cmd == 'quit':
            cs.stop()
            threading.Event().wait(3)
            break
        logging.info(threading.enumerate())
        logging.info(cs.clients)


if __name__ == '__main__':
    main()
    

2.2 客户端编程

2.2.1 客户端编程步骤

  • 创建socket对象
  • 连接到远端服务端的ip和port,connect()方法
  • 传输数据:使用send、recv方法发送、接收数据
  • 关闭连接,释放资源
import socket


client = socket.socket()
ip_address = ('127.0.0.1', 9999)
client.connect(ip_address)  # 直接连接服务器

client.send(b'abc\n')
data = client.recv(1024)  # 阻塞等待

client.close()

import socket
import threading
import datetime
import logging


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


class ChatClient:
    def __init__(self, ip='127.0.0.1', port=9999):
        self.client = socket.socket()
        self.address = ip, port
        self.event = threading.Event()

    def start(self):
        self.client.connect(self.address)
        self.send("I'm ready.")
        threading.Thread(target=self.rec, name='rec').start()

    def rec(self):
        while not self.event.is_set():
            try:
                data = self.client.recv(1024)  # 此句可能会出现异常,如网络中断
            except Exception as e:
                logging.error(e)
                break
                
            msg = "{:%Y/%m/%d %H:%M:%S} [{}:{}] {}".format(datetime.datetime.now(), *self.address, data)
            logging.info(msg)

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

    def stop(self):
        self.client.close()
        self.event.wait(3)
        self.event.set()
        logging.info('Client stops')


cc = ChatClient()
cc.start()

while True:
    cmd = input(">>>").strip()
    if cmd == 'quit':
        cc.stop()
        break
    cc.send(cmd)

内容源码均在github中的python仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值