TCP编程与Socket 深藏阁楼爱情的钟 2022-01-16 13:57 285阅读 0赞 # TCP编程与Socket # ## Socket介绍 ## Socket套接字 Python中提供socket.py标准库,非常底层的接口库。 Socket是一种通用的网络编程接口,和网络层次没有一一对应关系。 * **协议族** AF表示Address Family,用于socket()第一个参数 <table> <thead> <tr> <th align="left">名称</th> <th align="left">含义</th> </tr> </thead> <tbody> <tr> <td align="left">AF_INET</td> <td align="left">IPV4</td> </tr> <tr> <td align="left">AF_INET6</td> <td align="left">IPV6</td> </tr> <tr> <td align="left">AF_UNIX</td> <td align="left">Unix Domain Socket,windows没有</td> </tr> </tbody> </table> * **Socket类型** <table> <thead> <tr> <th align="left">名称</th> <th align="left">含义</th> </tr> </thead> <tbody> <tr> <td align="left">SOCK_STREAM</td> <td align="left">面向链接的流套接字。默认值,TCP协议</td> </tr> <tr> <td align="left">SOCK_DGRAM</td> <td align="left">无链接的数据报文套接字。UDP协议</td> </tr> </tbody> </table> 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][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_002] * 客户端连接输入: ![socket\_003][socket_003] ### socket常用属性 ### <table> <thead> <tr> <th align="left">名称</th> <th align="left">含义</th> </tr> </thead> <tbody> <tr> <td align="left"><strong>socket.getpeername()</strong></td> <td align="left">返回链接套接字的远程地址。返回值通常是元组(ipaddr,port)</td> </tr> <tr> <td align="left"><strong>socket.getsockname()</strong></td> <td align="left">返回套接字自己的地址。通常是一个元组(ipaddr,port)</td> </tr> <tr> <td align="left">socket.setblocking(flag)</td> <td align="left">如果flag为0,则将套接字设为非阻塞模式,否则套接字设为阻塞模式(默认值)<br>非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常</td> </tr> <tr> <td align="left">socket.settimeout(value)</td> <td align="left">设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。<br>一般,超时期应该在刚创建套接字时设置,因为他们可能用于连接的操作(如connect())</td> </tr> <tr> <td align="left">socket.setsockopt(level,optname,value)</td> <td align="left">设置套接字选项的值。比如缓冲区太小。太多了,不同系统,不同版本都不尽相同(详细请看文档)</td> </tr> </tbody> </table> ### socket常用方法 ### <table> <thead> <tr> <th align="left">名称</th> <th align="left">含义</th> </tr> </thead> <tbody> <tr> <td align="left"><strong>socket.recv(bufsize[,flags])</strong></td> <td align="left">获取数据。默认是阻塞的方式</td> </tr> <tr> <td align="left"><strong>socket.revfrom(bufsize[,flags])</strong></td> <td align="left">获取数据,返回一个二元组(bytes,address)</td> </tr> <tr> <td align="left">socket.recv_into(buffer[,nbytes[,flags]])</td> <td align="left">获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或0,将buffer大小的数据存入buffer中。返回接受的字节数。</td> </tr> <tr> <td align="left">socket.recfrom_into(buffer[,nbytes[,flags]])</td> <td align="left">获取数据,返回一个二元组(bytes,address)到buffer中</td> </tr> <tr> <td align="left"><strong>socket.send(bytes[,flags])</strong></td> <td align="left">TCP发送数据</td> </tr> <tr> <td align="left">socket.sendall(bytes[,flags])</td> <td align="left">TCP发送全部数据,成功返回None</td> </tr> <tr> <td align="left"><strong>socket.sendto(string[,flag],address)</strong></td> <td align="left">UDP发送数据</td> </tr> <tr> <td align="left">socket.sendfile(file,offset=0,count=None)</td> <td align="left">发送一个文件直到EOF,使用高性能的os.sendfile机制,返回发送的字节数。如果win下不支持sendfile,或者不是普通文件,使用send()发送文件。offset告诉起始位置。3.5版本开始</td> </tr> </tbody> </table> ### 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_004] 客户端操作: ![socket\_005][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_006] 服务端输出为: ![socket\_007][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() [socket_001]: /images/20220114/3a016a24d16e4f40ae7146c533a48574.png [socket_002]: /images/20220114/03bc1bda227a4d17bbdc2e8da2ca447f.png [socket_003]: /images/20220114/db12b6b0be4249de842bf47321a97dde.png [socket_004]: /images/20220114/9cfca715767147eb9fa9902a0c4cef5b.png [socket_005]: /images/20220114/760470551d69413d8e1d2b154445add5.png [socket_006]: /images/20220114/eae4771ad8244737a2491bf355f65b0c.png [socket_007]: /images/20220114/42b3fd80b8be494d881531e9d222c442.png
还没有评论,来说两句吧...