天天看點

網絡程式設計之TCP程式設計(socket、服務端、用戶端)1. socket介紹2. TCP程式設計

1. socket介紹

1.1 socket套接字

python中提供socket.py标準庫,非常底層的接口庫

socket是一種通用的網絡程式設計接口,和網絡層次沒一一對應的關系

1.2 協定族

AF表示Address Family,用于socket()第一個參數

網絡程式設計之TCP程式設計(socket、服務端、用戶端)1. socket介紹2. TCP程式設計

1.3 Socket類型

網絡程式設計之TCP程式設計(socket、服務端、用戶端)1. socket介紹2. TCP程式設計

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程式設計(socket、服務端、用戶端)1. socket介紹2. 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常用方法

網絡程式設計之TCP程式設計(socket、服務端、用戶端)1. socket介紹2. TCP程式設計
網絡程式設計之TCP程式設計(socket、服務端、用戶端)1. socket介紹2. TCP程式設計

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倉庫