TCP编程与Socket

TCP编程与Socket

Socket介绍

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

  • 协议族
    AF表示Address Family,用于socket()第一个参数
名称 含义
AF_INET IPV4
AF_INET6 IPV6
AF_UNIX Unix Domain Socket,windows没有
  • Socket类型
名称 含义
SOCK_STREAM 面向链接的流套接字。默认值,TCP协议
SOCK_DGRAM 无链接的数据报文套接字。UDP协议
  1. socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) #构建一个socket对象

TCP编程

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

TCP服务端编程

  1. 创建Socket对象
  2. 绑定IP地址Address和端口Port。bind()方法。(IPV4地址位一个二元组(“IP地址字符串”,Port))
  3. 获取用于传送数据的Socket对象
    • socket.accept()–>(socket object,addreass info)
    • accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组
    • 地址是远程客户端的地址,IPV4中它是一个二元组(clientaddr,prot)
      • 接受数据
        • revc(bufzize[,flags])使用缓冲区接受数据
      • 发送数据
        • send(bytees)发送数据

socket_001

  • 简单示例,创建一个简单的服务,实现监听一次,接受一次用户输出,并传递一个消息给用户。
import socket

#创建一个socket
service = socket.socket()
#绑定ip和端口
service.bind(("127.0.0.1",3999))
#启动监听
service.listen()
#等待用户接入
clsock,client = service.accept()
print("用户已近链接")
#等待用户发送消息
date = clsock.recv(1024)
print("收到用户发来消息:{}".format(date))
clsock.send("service -> {}".format(date).encode())

clsock.close() #和用户断开链接
service.close() #关闭service
print("服务器已近关闭")
  • 服务端输出:
    socket_002
  • 客户端连接输入:
    socket_003

socket常用属性

名称 含义
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) 设置套接字选项的值。比如缓冲区太小。太多了,不同系统,不同版本都不尽相同(详细请看文档)

socket常用方法

名称 含义
socket.recv(bufsize[,flags]) 获取数据。默认是阻塞的方式
socket.revfrom(bufsize[,flags]) 获取数据,返回一个二元组(bytes,address)
socket.recv_into(buffer[,nbytes[,flags]]) 获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或0,将buffer大小的数据存入buffer中。返回接受的字节数。
socket.recfrom_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版本开始

MakeFile

  1. socket.makefile(mode=‘r’, buffering=None, *, encoding=None, errors=None, newline=None) #创建一个与该套接字相关连的文件对象,将recv方法看做读方法,将send方法看做写方法。
    • mode:文件对象的模式,只支持"wra"三种模式的组合,不支持"w+"等,带加号这种。
    • buffering:缓存区大小
    • encoding:编码
    • errors:什么样的编码错误将被捕获。默认值为None
    • nowline:换行符
  • 注意:socket.makefile构造方法中的参数与open对象参数大致相同。
  • 注意:makefile实质是在内存中构建了一个文件对象。文件并不是真的存在。类似于StringIO
  • 简单示例:
import socket

server = socket.socket()
server.bind(("127.0.0.1",3999))
server.listen()
print("- "* 30)

s,_ = server.accept()
print(" -"* 30)
f = s.makefile(mode="rw")

#读满10个字符就不阻塞了
line = f.read(10) #按照行读取,要使用readline方法。
print("- "* 30)
print(line)
f.write("server -> {}".format(line))
f.flush()

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

server.close()

服务端输出结果:
socket_004
客户端操作:
socket_005

实战–写一个群聊天service

  • 需求分析
    1. 聊天工具是CS程序,C是每一个客户端client,S是服务器端server。
  • 服务器应该具有的功能:
    1. 启动服务,包括绑定地址和端口,并监听
    2. 建立连接,能和多个客户端建立连接
    3. 接收不同用户的信息
    4. 分发,将接收的某个用户的信息转发到已连接的所有客户端
    5. 停止服务
    6. 记录连接的客户端
  • 代码实现
import socket
import threading
import logging
import sys

log = logging.getLogger(__name__)
hd = logging.StreamHandler(sys.stdout)
hd.setFormatter(logging.Formatter("[{asctime},{thread},{threadName},{name}] {message}",style="{",datefmt="%Y-%m-%d %H:%M:%S"))
log.addHandler(hd)
log.setLevel(logging.INFO)

class ChatServer:
    def __init__(self,ip="127.0.0.1",port=3999):
        self.sock = socket.socket()
        self.ip = ip
        self.port = port
        self.clients = {} #客户端
        self.lock = threading.Lock()
        self.event = threading.Event()

    def start(self):#启动服务
        threading.Thread(target=self.accept).start()

    def accept(self):#支出多少人链接
        self.sock.bind((self.ip, self.port))
        self.sock.listen()
        while not self.event.is_set():
            try:
                newsock,client = self.sock.accept()
            except:
                break
            log.info("新用户加入:{}".format(client))
            with self.lock:
                self.clients[client] = newsock
            threading.Thread(target=self.recv,args=(newsock,client)).start()

    def recv(self,sock:socket.socket,client): #接受客户端数据
        while not self.event.is_set():
            try:
                data = sock.recv(1024)
            except Exception as e:
                log.error(e)
                data = b""
            log.info(data)
            if data == b"by" or data == b"":
                with self.lock:
                    if client in self.clients:
                        self.clients.pop(client)
                sock.close()
                break
            msg = "service:{}-->{}".format(client,data).encode()
            exps = [] #记录sock错误
            expc = [] #记录sock出错时对应的clients
            with self.lock:
                for c,sk in self.clients.items():
                    try:
                        sk.send(msg) #可能在发送消息是就出错
                    except:
                        exps.append(sk)
                        expc.append(c)
                for c in expc:
                    self.clients.pop(c)
            for s in exps:
                s.close()

    def stop(self): #停止服务
        self.event.set()
        self.sock.close()
        clis = []
        with self.sock:
            clis = [ i for i in self.clients.values()]
            self.clients.clear()
        for s in clis:
            s.close()

if __name__ == "__main__":
    chaser = ChatServer()
    chaser.start()
    while True:
        inp = input(">>>")
        if inp == "quit":
            chaser.stop()
            break
        else:
            log.info(threading.enumerate())

  • 客户端主动断开带来的问题
    1. 服务端知道自己何时断开,如果客户端断开,服务器不知道。(客户端主动断开,服务端recv会得到一个空串) 所以,好的做法是,客户端断开发出特殊消息通知服务器端断开连接。但是,如果客户端主动断开,服务端主动发送一个空消息,超时返回异常,捕获异常并清理连接。
    2. 即使为客户端提供了断开命令,也不能保证客户端会使用它断开连接。但是还是要增加这个退出功能。
  • 注意
    1. 由于GIL和内置数据结构的读写原子性,单独操作字典的某一项item是安全的。但是遍历过程是线程不安全的,遍 历中有可能被打断,其他线程如果对字典元素进行增加、弹出,都会影响字典的size,就会抛出异常。所以还是要 加锁Lock。

TCP客户端编程

  • 客户端编程步骤

    1. 创建Socket对象
    2. 链接到远端服务端的ip和port,connect()方法
    3. 传输数据
      • 使用send,recv方法发送,接收数据
    4. 关闭链接,释放资源
  • 简单示例

import socket

client = socket.socket()
client.connect(("127.0.0.1",3999)) #直接链接服务器

client.send(b"abcd")
data = client.recv(1024) #柱塞等待服服务端回复
print(data)

client.close()

结合上面服务端代码。客户端输出为:
socket_006
服务端输出为:
socket_007

  • 使用类封装客户端代码
import logging
import sys
import socket
import threading
import datetime

log = logging.getLogger(__name__)
hd = logging.StreamHandler(sys.stdout)
hd.setFormatter(logging.Formatter("[{asctime},{thread},{threadName},{name}] {message}",style="{",datefmt="%Y-%m-%d %H:%M:%S"))
log.addHandler(hd)
log.setLevel(logging.INFO)

class ChatClient:
    def __init__(self,ip="127.0.0.1",port=3999):
        self.sock = socket.socket()
        self.ip = ip
        self.port = port
        self.event = threading.Event()

    def start(self):
        self.sock.connect((self.ip,self.port))
        self.send("I am reader")
        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:
                log.error(e)
                break
            msg = "{:%Y-%m-%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),self.ip,self.port,data.strip())
            log.info(msg)

    def send(self,msg):
        data = msg.encode()
        self.sock.send(data)

    def stop(self):
        self.event.set()
        self.sock.close()
        log.info("客户端已经关闭")

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

if __name__ == '__main__':
    main()

猜你喜欢

转载自blog.csdn.net/u013008795/article/details/92196243