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协议 |
- socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) #构建一个socket对象
TCP编程
Socket编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server,客户端称为Client
这种编程模式也称为cs编程
TCP服务端编程
- 创建Socket对象
- 绑定IP地址Address和端口Port。bind()方法。(IPV4地址位一个二元组(“IP地址字符串”,Port))
- 获取用于传送数据的Socket对象
- socket.accept()–>(socket object,addreass info)
- accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组
- 地址是远程客户端的地址,IPV4中它是一个二元组(clientaddr,prot)
- 接受数据
- revc(bufzize[,flags])使用缓冲区接受数据
- 发送数据
- send(bytees)发送数据
- 接受数据
- 简单示例,创建一个简单的服务,实现监听一次,接受一次用户输出,并传递一个消息给用户。
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常用属性
名称 | 含义 |
---|---|
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
- 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()
服务端输出结果:
客户端操作:
实战–写一个群聊天service
- 需求分析
- 聊天工具是CS程序,C是每一个客户端client,S是服务器端server。
- 服务器应该具有的功能:
- 启动服务,包括绑定地址和端口,并监听
- 建立连接,能和多个客户端建立连接
- 接收不同用户的信息
- 分发,将接收的某个用户的信息转发到已连接的所有客户端
- 停止服务
- 记录连接的客户端
- 代码实现
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())
- 客户端主动断开带来的问题
- 服务端知道自己何时断开,如果客户端断开,服务器不知道。(客户端主动断开,服务端recv会得到一个空串) 所以,好的做法是,客户端断开发出特殊消息通知服务器端断开连接。但是,如果客户端主动断开,服务端主动发送一个空消息,超时返回异常,捕获异常并清理连接。
- 即使为客户端提供了断开命令,也不能保证客户端会使用它断开连接。但是还是要增加这个退出功能。
- 注意
- 由于GIL和内置数据结构的读写原子性,单独操作字典的某一项item是安全的。但是遍历过程是线程不安全的,遍 历中有可能被打断,其他线程如果对字典元素进行增加、弹出,都会影响字典的size,就会抛出异常。所以还是要 加锁Lock。
TCP客户端编程
-
客户端编程步骤
- 创建Socket对象
- 链接到远端服务端的ip和port,connect()方法
- 传输数据
- 使用send,recv方法发送,接收数据
- 关闭链接,释放资源
-
简单示例
import socket
client = socket.socket()
client.connect(("127.0.0.1",3999)) #直接链接服务器
client.send(b"abcd")
data = client.recv(1024) #柱塞等待服服务端回复
print(data)
client.close()
结合上面服务端代码。客户端输出为:
服务端输出为:
- 使用类封装客户端代码
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()