python -- 网络编程(TCP编程)

TCP编程

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

TCP服务端编程

服务端编程步骤

  • 创建Socket对象

  • 绑定IP地址Address和端口Port。使用bind()方法
    IPv4地址为一个二元组('IP地址字符串‘, Port)

  • 开始监听,将再指定的IP端口上监听。使用listen()方法

  • 获取用于传送数据的socket对象
    socket.accrpt() -> (socket object, address info)
    accept方法阻塞等待客户端创立连接,返回一个新的Socket对象和客户端地址的二元组
    地址是远程客户端的地址,IPv4中它是一个二元组(clientaddr, port)
    接受数据

    • recv(bufsize[, flags]) 使用缓冲区接受数据

    发送数据

    • send(bytes) 发送数据,必须传入一个bytes

在这里插入图片描述

问题

两次绑定同一个监听端口会怎么样?

import socket

s = socket.socket() # 创建socket对象,TCP连接 IPv4
ip = '127.0.0.1' # 本机回环地址
port = 9999 # TCP 65536
s.bind((ip, port)) # 一个地址和端口二元组
s.listen() # 开始监听,等待客户端连接到来

# 接入一个到来的连接
s1, info = s.accept() # 阻塞直到和客户端成功建立连接,返回一个新的socket对象和客户端地址
print(s1, info)

# 使用缓冲区获取诗句
data = s1.recv(1024) # 阻塞
print(info, data)
s1.send(b'hello world')
s1.close()

# 接入另一个连接
s2, info = s.accept()
data = s2.recv(1024)
print(info, data)
s2.send(b'hello python')
s2.close()

s.close()

上例accept和recv是阻塞的,主线程经常被阻塞住而不能工作

客户端操作

使用测试工具模拟客户端进行测试
在这里插入图片描述

  • 灰色,需点连接才可成功连接
    在这里插入图片描述
查看监听端口
  • windoes 命令
    netstat -anp tcp | findstr 9999
  • linux 命令
    netstat -tanl | grep 9999

    ss -tanl | grep 9999
    ss命令没有可使用pip install ss自行安装

注意

  • IP 127.0.0.1 指本机回环地址,永远指向本机
  • port 端口 linux 用1000以上,这是一个两字节数,范围[0,65535] ,共65536种状态,当前此协议的此端口不能被别人占用
  • server.bild() 绑定一个二元组,不可多次绑定同一个端口
  • listen() 监听,不可多次监听,真正的显示出端口
  • 打开的资源必须关闭,会占用文件描述符 fd

实现服务端主动断开连接 收发各一次

# TCP Server 端开发
import socket

server = socket.socket() # TCP 连接 IPv4
ip = '127.0.0.1'
port = 9999 # TCP 65536
server.bind((ip, port)) # address

server.listen() # 真正的显示出端口
print(server)

newsock, clientinfo = server.accept() # 默认阻塞


data = newsock.recv(1024) # 阻塞,buffer空
print(data)
newsock.send('server acl. data={}'.format(data).encode()) # buffer 满
newsock.close()

server.close()

执行结果

  • 服务端控制台结果
<socket.socket fd=508, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999)>
b'hello world'

Process finished with exit code 0
  • 客户端结果
    在这里插入图片描述

服务端循环接受客户端信息

# TCP Server 端开发
import socket

server = socket.socket() # TCP 连接 IPv4
ip = '127.0.0.1'
port = 9999 # TCP 65536
server.bind((ip, port)) # address

server.listen() # 真正的显示出端口
print(server)

newsock, clientinfo = server.accept() # 默认阻塞
print(newsock)
print('new1', clientinfo)
while True:
    data = newsock.recv(1024) # 阻塞,buffer空
    print(data)
    newsock.send('server acl. data={}'.format(data).encode()) # buffer 满
newsock.close()

server.close()

执行结果

  • 服务端控制台结果
<socket.socket fd=552, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999)>
<socket.socket fd=556, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999), raddr=('127.0.0.1', 54481)>
new1 ('127.0.0.1', 54481)
b'hello world'
b'hello python'
b''
Traceback (most recent call last):
  File "C:/Users/mayn/PycharmProjects/mypython/m/t1.py", line 16, in <module>
    data = newsock.recv(1024) # 阻塞,buffer空
ConnectionAbortedError: [WinError 10053] 你的主机中的软件中止了一个已建立的连接。

Process finished with exit code 1
  • 客户端结果
    在这里插入图片描述

因上例代码中未设置循环退出条件,故手动中断连接会抛异常

实战 – 写一个群聊程序

需求分析

聊天工具是CS程序,C是每一个客户端client,S是服务器端server。
服务器应该具有的功能:

  1. 启动服务,包括绑定地址和端口,并监听
  2. 建立连接,能和多个客户端建立连接
  3. 接收不同用户的信息
  4. 分发,将接收的某个用户的信息转发到已连接的所有客户端
  5. 停止服务
  6. 记录连接的客户端
代码实现
  • 服务端应该对应一个类
class ChatServer:
    def __init__(self, ip, port): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)

    def start(self): # 启动监听
        pass

    def accept(self): # 多人连接
        pass

    def recv(self): # 接受客户端数据
        pass

    def stop(self): # 停止服务
        pass
  • 在此基础上,扩展完成
import logging
import socket
import threading
import datetime

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


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)
        self.clients = {} # 客户端

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新的线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while True:
            sock, client = self.sock.accept() # 阻塞
            self.clients[client] = sock # 添加到客户端字典
            # 准备接受数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv, args=(sock, client)).start()

    def recv(self, sock:socket.socket, client): # 接受客户端数据
        while True:
            data = sock.recv(1024) # 阻塞到数据到来
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(
                datetime.datetime.now(), *client, data.decode())
            logging.info(msg)
            msg = msg.encode()
            for s in self.clients.values():
                s.send(msg)
                
    def stop(self): # 停止服务
        for s in self.clients.values():
            s.close()
        self.sock.close()

cs = ChatServer()
cs.start()
  • 基本功能完成,但是有问题。使用Event改进
import logging
import socket
import threading
import datetime

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


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)
        self.clients = {} # 客户端
        self.event = threading.Event()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新的线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock, client = self.sock.accept() # 阻塞
            self.clients[client] = sock # 添加到客户端字典
            # 准备接受数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv, args=(sock, client)).start()

    def recv(self, sock:socket.socket, client): # 接受客户端数据
        while not self.event.is_set():
            data = sock.recv(1024) # 阻塞到数据到来
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(
                datetime.datetime.now(), *client, data.decode())
            logging.info(msg)
            msg = msg.encode()
            for s in self.clients.values():
                s.send(msg)

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

cs = ChatServer()
cs.start()

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

这一版基本能用了,测试通过。但是还有要完善的地方
例如各种异常的判断,客户端断开连接后字典中的移除客户端数据等

  • 客户端主动断开带来的问题

    • 服务端知道自己何时断开,如果客户端断开,服务器不知道。(客户端主动断开,服务端recv会得到一个空串)
    • 所以,好的做法是,客户端断开发出特殊消息通知服务器端断开连接。但是,如果客户端主动断开,服务端主动发送一个空消息,超时返回异常,捕获异常并清理连接
    • 即使为客户端提供了断开命令,也不能保证客户端会使用它断开连接。但是还是要增加这个退出功能
  • 增加客户端退出命令

import logging
import socket
import threading
import datetime

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


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)
        self.clients = {} # 客户端
        self.event = threading.Event()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新的线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock, client = self.sock.accept() # 阻塞
            self.clients[client] = sock # 添加到客户端字典
            # 准备接受数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv, args=(sock, client)).start()

    def recv(self, sock:socket.socket, client): # 接受客户端数据
        while not self.event.is_set():
            data = sock.recv(1024) # 阻塞到数据到来
            msg = data.decode().strip()
            # 客户端退出命令
            if msg == 'quit' or msg == '': # 主动断开得到空串
                self.clients.pop(client)
                sock.close()
                logging.info('{} quits'.format(client))
                break

            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(
                datetime.datetime.now(), *client, data.decode())
            logging.info(msg)
            msg = msg.encode()
            for s in self.clients.values():
                s.send(msg)

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

cs = ChatServer()
cs.start()

while True:
    cmd = input('>>').strip()
    if cmd =='quit':
        cs.stop()
        threading.Event().wait(3)
        break
    logging.info(threading.enumerate()) # 用来观察断开后线程的变化

程序还是有瑕疵,但是业务基本功能完成了
注意:

  • 由于GIL和内置数据结构的读写原子性,单独操作字典的某一项item是安全的。但是遍历过程是线程不安全的,遍历中有可能被打断,其他线程如果对字典元素进行增加、弹出,都会影响字典的size,就会抛出异常。所以还是要加锁Lock
  • 加锁后代码如下
import logging
import socket
import threading
import datetime

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


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)
        self.clients = {} # 客户端
        self.event = threading.Event()
        self.lock = threading.Lock()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新的线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock, client = self.sock.accept() # 阻塞
            with self.lock:
                self.clients[client] = sock # 添加到客户端字典
            # 准备接受数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv, args=(sock, client)).start()

    def recv(self, sock:socket.socket, client): # 接受客户端数据
        while not self.event.is_set():
            data = sock.recv(1024) # 阻塞到数据到来
            msg = data.decode().strip()
            # 客户端退出命令
            if msg == 'quit' or msg == '': # 主动断开得到空串
                with self.lock:
                    self.clients.pop(client)
                    sock.close()
                logging.info('{} quits'.format(client))
                break

            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(
                datetime.datetime.now(), *client, data.decode())
            logging.info(msg)
            msg = msg.encode()

            with self.lock:
                for s in self.clients.values():
                    s.send(msg)

    def stop(self): # 停止服务
        self.event.set()
        with self.lock:
            for s in self.clients.values():
                s.close()
        self.sock.close()

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)

socket常用方法

名称含义
socket.recv(bufsize[, flags])获取数据。默认是阻塞的方式
socket.recvfrom(bufsize[, flags])获取数据,返回一个二元组(bytes, address)
socket.recv_into(buffer[, nbytes[, flags]])获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或0,将buffer大小的数据存入buffer中。返回接收的字节数
socket.recvfrom_into(buffer[, nbytes[, flags]])获取数据,返回一个二元组(bytes, address)到buffer中
socket.send(bytes[, flags])TCP发送数据
socket.sendall(bytes[, flags])TCP发送全部数据,成功返回None
socket.sendto(string[,flag],address)UDP发送数据
socket.sendfile(file, offset=0, count=None)发送一个文件直到EOF,使用高性能的os.sendfile机制,返回发送的字节数。如果win下不支持sendfile,或者不是普通文件,使用send()发送文件。offset告诉起始位置。3.5版本开始
名称含义
socket.getpeername()返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)
socket.getsockname()返回套接字自己的地址。通常是一个元组(ipaddr,port)
socket.setblocking(flag)如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)
非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
socket.settimeout(value)设置套接字操作的超时期,timeout是一个浮点数,单位是秒
值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect()
socket.setsockopt(level,optname,value)设置套接字选项的值。比如缓冲区大小。太多了,去看文档
不同系统,不同版本都不尽相同

MakeFile

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

import socket

server = socket.socket()
server.bind(('127.0.0.1', 9999))
server.listen()

s, _ =server.accept()
f = s.makefile(mode='rw')

print(s.getpeername())
print(s.getsockname())

f = s.makefile('rw')
data = f.read(10) # 按行读取要使用readline方法
print(data)
f.write('return your msg: {}'.format(data))
f.flush()

f.close()
print(f.closed, s._closed)
s.close()
print(f.closed, s._closed)

server.close()
  • 服务端控制台执行结果
('127.0.0.1', 62161)
('127.0.0.1', 9999)
1234567890
True False
True True

Process finished with exit code 0
  • 客户端执行结果
    在这里插入图片描述
makefile练习
  • 使用makefile改写群聊类
import logging
import socket
import threading
import datetime

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

class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)
        self.clients = {} # 客户端
        self.event = threading.Event()
        self.lock = threading.Lock()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,开一个新线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock, client = self.sock.accept() # 阻塞
            f = sock.makefile('rw') # 支持读写
            with self.lock:
                self.clients[client] = f, socket # 添加到客户端字典
            # 准备接受数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv, args=(f, client)).start()

    def recv(self, f, client): # 接受客户端数据
        while not self.event.is_set():
            data = f.readline() # 阻塞等一行来,即换行符
            msg = data.strip()
            print(msg, '~~~~~~~~~~~')
            # 客户端退出命令
            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 {}:{}\n{}\n}".format(
                datetime.datetime.now(), *client, data)
            logging.info(msg)

            with self.lock:
                for ff,_ in self.clients.values():
                    ff.write(msg)
                    ff.flush()

    def stop(self): # 停止服务
        self.event.set()
        with self.lock:
            for f, s in self.clients.values():
                f.close()
                s.close()
        self.sock.close()

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)

上例完成了基本功能,但是,如果客户端主动断开,或者readline出现异常,就不会从clients中移除作废的socket。可以使用异常处理解决这个问题

ChatServer实验用完整代码

注意,这个代码为实验用,代码中瑕疵还有很多。Socket太底层了,实际开发中很少使用这么底层的接口。
增加一些异常处理。

import logging
import socket
import threading
import datetime

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

class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip, port)
        self.clients = {} # 客户端
        self.event = threading.Event()
        self.lock = threading.Lock()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept 会阻塞主线程,所以开一个新线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock, client = self.sock.accept() # 阻塞
            f = sock.makefile('rw') # 支持读写
            with self.lock:
                self.clients[client] = f, sock # 添加到客户端字典
            # 准备接收收据, recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv, args=(f, client)).start()

    def recv(self, f, client): # 接收客户端数据
        while not self.event.is_set():
            try: # 异常处理
                data = f.readline() # 阻塞等一行来,换行符
            except Exception as e:
                logging.error(e)
                data = 'quit'

            msg = data.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} {}:{}\n{}\n'.format(
                datetime.datetime.now(), *client, data)
            logging.info(msg)

            with self.lock:
                for ff,_ in self.clients.values():
                    ff.write(msg)
                    ff.flush()

    def stop(self): # 停止服务
        self.event.set()
        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()

TCP客户端编程

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


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

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

client.close()
  • 开始编写客户端类
import socket
import threading
import datetime
import logging

FORMAT = "%(asctime)s %(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.sock = socket.socket()
        self.addr = (ip, port)
        self.event = threading.Event()

    def start(self): #  启动对远端服务器的链接
        self.sock.connect(self.addr)
        self.send("I'm ready.")
        # 准备接收数据,recv是阻塞的,开启新的线程
        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
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(
                datetime.datetime.now(), *self.addr, data.strip())
            logging.info(msg)

    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 stops.')

def main():
    cc = ChatClient()
    cc.start()
    while True:
        cmd = input('>>')
        if cmd.strip() == 'quit':
            cc.stop()
            break
        cc.send(cmd) # 发送消息

if __name__ == '__main__':
    main()

同样,这样的客户端还是有些问题的,仅用于测试

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值