socket = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
名称 | 含义 |
---|---|
AF_INEET | IPV4(默认值) |
AF_INET6 | IPV6 |
AF_UNIX | Unix Domain Socket, windows没有 |
名称 | 含义 |
---|---|
SOCK_STREAM | 面向连接的流套接字, 即TCP协议(默认值) |
SOCKET_DGRAM | 无连接的数据报文套接字。即UDP协议 |
socket编程需要两端,一般来说,需要一个服务端(Server),一个客户端(Client)。
这种编程模式也称为CS编程
socket对象常用方法:
方法 | 含义 |
---|---|
socket.getpeername | 返回套接字的远程地址,返回值是raddr |
socket.getsocketname | 返回套接字自己的地址,返回值为laddr |
构建一个TCP协议的远古版本群聊服务端
import datetime
import socket
import threading
class ChatSocket:
def __init__(self, ip="127.0.0.1", port=9999): # 本地localhost也可以用""或者"localhost"表示
self.addr = (ip, port)
self.socket = socket.socket() # 创建socket对象
self.clients = {} # 创建客户端词典
self.event = threading.Event()
self.lock = threading.Lock() # 设置一把锁,为了遍历客户端的字典的时候保证线程安全
def start(self):
self.socket.bind(self.addr) # 绑定端口
self.socket.listen(2) # 开始监听,监听客户端无上限
threading.Thread(target=self.accept).start() # 如果不创建此线程,下面的等待连接accept会阻塞当前的主线程,导致无法执行最后的stop
def accept(self):
while not self.event.is_set(): # 标志位的设置是为了stop时,不再 等待新线程的接入
new_socket, client_info = self.socket.accept() # 等待连接,阻塞,多人连接
with self.lock:
self.clients[client_info] = new_socket
print(client_info, "已连接")
t = threading.Thread(target=self.recv_send, args=(new_socket, client_info)) # 由于recv会发生阻塞行为
t.start()
def recv_send(self, new_socket, clint_info):
while not self.event.is_set():
data = new_socket.recv(1024) # 设置一次最大读取数据上限是1024字节,阻塞,如果客户端主动断开连接,会收到空的字符串
print(data)
if data.decode().strip() == "quit" or data.decode().strip() == "": # 客户端上的退出按钮会返回一个空字符串或者发送quit退出
print("用户", clint_info[1], "quit")
with self.lock:
self.clients.pop(clint_info)
new_socket.close()
break
msg = "用户{} {}说: {}".format(clint_info[1], datetime.datetime.now(), data.decode(encoding="gbk"))
print(msg)
with self.lock:
for s in self.clients.values(): # 群聊的性质就是将从client接收的信息,转发给当前所有连接到server的client客户端,是一种很,标识符的选择一定要慎重
s.send(msg.encode(encoding="gbk"))
def stop(self):
self.event.set() # 设为True
with self.lock:
for s in self.clients.values():
s.close()
self.socket.close()
cs = ChatSocket()
cs.start()
while True:
cmd = input(">>").strip()
if cmd == "quit":
cs.stop()
threading.Event().wait(3)
break
再构建一个客户端
客户端连接步骤:
import socket
import threading
import logging
fmt = "%(asctime)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(format=fmt, level=logging.INFO)
class Client:
def __int__(self, ip="127.0.0.1", port=9999):
self.sock = socket.socket() # 建立socket对象
self.addr = ip, port
self.event = threading.Event()
def start(self):
self.sock.connect(self.addr)
self.sock.send("i'm read")
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
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 stop..")
def main():
cc = Client()
cc.start()
while True:
cmd = input(">>input msg").strip()
if cmd == "quit":
cc.stop()
break
cc.send(cmd)
if __name__ == "__main__":
main()
UDP编程和上面的TCP编程有些不同,主要体现在协议本身的不同
UDP是无连接协议,所以可以只有任何一端,例如客户端数据发往服务端,服务端存在与否无所谓。
注意:
UDP的socket多想创建后,是没有占用本地址和端口的
UDP协议的常见方法:
方法 | 说明 |
---|---|
bind | 可以指定地址和端口,会立即占用 |
connect | 可以立即占用本地地址和端口laddr,填充远端地址和端口raddr |
sendto | 可以立即占用本地地址和端口laddr,并把数据发往指定远端,只要有了本地地址和端口,sendto就可以像任何远端发送数据 |
send | 需要和connect配合,可以使用已经从本地端口把数据发送raddr指定的远端 |
recv | 要求一定占用了本地端口后,返回接收的数据 |
recvfrom | 要求一定占用了本地端口后,返回接收的数据和对端地址的二元组 |
# echoserver模型的聊天室服务端
import threading
import socket
import time
class ChatServerUDP:
def __init__(self, ip="127.0.0.1", port=9999, deadtime=10):
self.addr = ip,port
self.sock = socket.socket(type=socket.SOCK_DGRAM)
self.event = threading.Event()
self.clients = {}
self.deadtime = deadtime
def start(self):
self.sock.bind(self.addr)
threading.Thread(target=self.recv).start()
def recv(self):
while not self.event.is_set():
deadclient = set() # 用来处理死了的客户端
data, raddr = self.sock.recvfrom(1024)
if data == b"==heatbeat==": # 如果接收到心跳包,说明客户端还活着
current = time.time()
self.clients[raddr] = current
continue
if data == b"quit": # 注意这里,是bytes字节流,千万别搞错
self.clients.pop(raddr)
continue
current = time.time()
self.clients[raddr] = current
print(data.decode())
msg = "hello"
for c, stamp in self.clients.items():
if time.time() - stamp < self.deadtime: # 这里是用10秒表示过期时长, 可以自己调控
self.sock.sendto(msg.encode(), c) # 只向没有过期的client的发送data
else:
deadclient.add(c)
for c in deadclient: # 将过期的client清除掉
self.clients.pop(c)
def stop(self):
self.event.set()
self.sock.close()
def main():
c = ChatServerUDP()
c.start()
while True:
cmd = input(">>请输入:").strip()
if cmd == "quit":
c.stop()
break
if __name__ == '__main__':
main()
import threading
import socket
import logging
fmt = "%(asctime)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(format=fmt, level=logging.INFO)
class ChatClientUDP:
def __init__(self,ip, port):
self.addr = ip, port
self.sock = socket.socket(type=socket.SOCK_DGRAM) # 选用UDP协议
self.event = threading.Event()
def start(self):
self.sock.connect(self.addr)
threading.Thread(target=self.recv).start()
threading.Thread(target=self.hb).start()
def hb(self): # 心跳包发送
while not self.event.wait(5): # 客户端没关闭,如果event一直是false的情况下每五秒返回一个false
self.send("==heatbeat==")
def recv(self):
while not self.event.is_set():
data, raddr = self.sock.recvfrom(1024) # 工作线程只负责就收信息
print("他说:",data.decode())
def send(self, msg:str):
self.sock.send(msg.encode())
def stop(self):
self.event.set()
self.send("quit")
self.sock.close()
def main():
c1 = ChatClientUDP("127.0.0.1", 6000)
c2 = ChatClientUDP("127.0.0.1", 6000)
c1.start()
c2.start()
while True:
cmd = input(">>请说:").strip()
if cmd == "quit":
c1.stop()
c2.stop()
break
c1.send(cmd) # 这里主线程用来信息
c2.send(cmd)
if __name__ == "__main__":
main()
1.一般来说,客户端定时发往服务端的,服务端并不要需要返回ack恢复给客户端,只需要记录该客户端或者就行了。
2.如果是服务端定时发往客户端的,一般需要客户端ack响应来表示或者,如果没收收到客户端的ack, 服务端就移除其信息。这种实现较为复杂,用的较少。
3.也可以双向发送心跳的。用的较少
上面的UDP的服务端和客户端使用了心跳机制,如果一个客户端或者就会按照指定的间隔发送心跳包,服务端每收到心跳包,就更新这个客户端的当前在线时间, 超时过期机制就会把超过指定间隔时间的客户端清理掉
区别 | TCP | UDP |
---|---|---|
连接类型 | 面向连接 | 无连接 |
可靠性 | 可靠 | 不可靠 |
数据包是否有序 | 数据包有序 | 数据包无序 |
使用场景 | 大多数需要数据可靠的场合 | 视频、音频等不需要数据可靠的地方 |
连接
可靠性
有序