服务器通信初试-视频通信

本文最后更新于:2024年3月7日 下午


这里是初试服务器通信的后续,添加了从服务器端到客户端的视频传输,也使用了tkinter制作了客户端的ui界面。


原理

TCP无法直接传输视频文件,需要将图片进行编码,使用的是opencv的cv2.imencode()函数,该函数可以将图片格式转换(编码)成流数据,赋值到内存缓存中;主要用于图像数据格式的压缩,方便网络传输。
相对的,客户端中就需要使用cv2.imdecode()函数来恢复图像。
若客户端申请视频链接,服务端根据客户端的请求类型会构造一个新的tcp连接,只负责视频传输。这样可以让视频和客户端的其他指令能够分线程传递,互不干扰。
客户端在原来的基础上增加了可视化界面,便于指令发送以及显示视频,具体是使用tkinter实现可以看下面的部分。
服务器端在客户端连接类里增加了视频传输,摄像头获取等功能。
具体的图片传输逻辑为:

1
2
3
4
5
服务端发送图片大小->
客户端收到图片大小->
服务端按每份1字节的信息量发送图片->
客户端每次接收一份1字节信息,直到获得完整的一张图片信息,随后进行展示,展示正常结束后发送继续发送指令给服务端->
服务端收到继续发送指令,继续发送下一张图片。

这样做的有一个缺陷是服务端得在客户端接受完一张图片的全部信息并处理之后返回接收成功消息之后才能进行下一张图片的发送,这样会导致服务器端从摄像头采集过来的许多未能发出的图片在内存里面产生堆积,挤占内存空间,同时也会造成客户端视频展示卡顿率极高。
使用了两个解决方案:
增大每份图片拆包的字节数目,每份1024字节,这样可以大幅度降低在传输时间上的损耗,使用了之后卡顿极大程度的缓解了。
在服务器端等待客户端回复的时间,用于清空摄像机缓存帧,使用cv.grab()可以只取帧不解码,可以用于快速处理无用帧。
(实际上在双创项目的单片机部分设想中多余的帧会存到sd卡上,每秒只会向服务器发指定帧数,也就不会存在这个问题)


实际上搜索了之后发现视频往往依靠udp协议传递,偶尔缺帧是可以承受的,若是之后改用udp传输,可能还需要在接收端增添缓存队列,这样也可以更加流畅。

客户端tkinter可视化

原本的信息发送仅仅只是做了接收框和发送框,以及绑定了个连接服务器和发送的按钮。并且确保发送和接收消息是不同线程。
信息传递的流程为:连接-输入框输入用户名-发送-连接成功!-自由通信。
关于视频传输的部分增添了个视频按钮,点击后也会和连接服务器一样创建新的连接线程(用的是创建线程执行程序,这方法还蛮好用的)。创建之后就会把接收过来的图片信息恢复成图像,然后显示到tkinter的画布上并刷新组件,达到视频效果。

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import tkinter as tk
import time,socket,threading
import cv2
import numpy
from PIL import ImageTk, Image

# 创建线程执行程序
def thread_it(func, *args): # 传入函数名和参数
# 创建线程
t = threading.Thread(target=func, args=args)
# 守护线程
t.setDaemon(True)
# 启动
t.start()

class ChatUI:
def __init__(self):
self.root = tk.Tk()
self.root.title("Chat")
self.root.geometry("400x400")
self.HOST = '127.0.0.1' #填你服务器的地址
self.PORT = 8881
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.vedio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.create_widgets()

def create_widgets(self):
# 创建信息显示框
self.text = tk.Text(self.root,height=7, width=30, bd=5)
self.text.place(x=0, y=0)
#self.text.pack()
# 创建信息输入框
self.entry = tk.Entry(self.root)
self.entry.place(x=0, y=103,width=225)
# 创建连接按钮
self.connect_button = tk.Button(self.root, text="连接", command=lambda:thread_it(self.connect_server))
self.connect_button.place(x=225, y=0,height=30,width=30)
# 创建发送按钮
self.send_button = tk.Button(self.root, text="发送", command=self.send)
self.send_button.place(x=225, y=30,height=30,width=30)
# 创建显示视频按钮
self.vedio_button = tk.Button(self.root, text="视频", command=lambda:thread_it(self.connect_vedio))
self.vedio_button.place(x=225, y=60,height=30,width=30)
# 创建视频页面
self.vedio_canvas = tk.Canvas(self.root,bg = 'white' )#绘制画布
self.vedio_canvas.place(x=0, y=133,height=650,width=700)
def recv(self):
username = self.entry.get()
self.sock.send(username.encode("utf-8"))
# 清空输入框
self.entry.delete(0, tk.END)
while True:
# 接收消息并显示在信息显示框中
data = self.sock.recv(1024).decode("utf-8")
if not data:
break
self.text.insert(tk.END, data)
self.text.insert(tk.END, '\n')

def send(self):
time.sleep(0.5)
while True:
message = self.entry.get()
print(message)
if message:
# 发送消息到服务器
self.sock.send(message.encode("utf-8"))
# 清空输入框
self.entry.delete(0, tk.END)
else:
break

def connect_server(self):
# 连接服务器
self.sock.connect_ex((self.HOST, self.PORT))
data='已找到空闲服务器\n请输入用户名\n'
self.text.insert(tk.END, data)
t1 = threading.Thread(target=self.recv, args=())
t1.start()
t1.join()
# 接受图片大小的信息

def recv_size(self, count):
buf = b''
while count:
newbuf = self.vedio_sock.recv(count)
if not newbuf: return None
buf += newbuf
count -= len(newbuf)
return buf

# 接收图片
def recv_all(self, count):
buf = ('').encode()
while count:
# 这里每次只接收一个字节的原因是增强python与C++的兼容性
# python可以发送任意的字符串,包括乱码,但C++发送的字符中不能包含'\0',也就是字符串结束标志位
newbuf = self.vedio_sock.recv(4096)
if not newbuf: return None
buf += newbuf
count -= len(newbuf)
buf_str=numpy.array(buf)
#print(buf_str)
return buf_str
def connect_vedio(self):
self.vedio_sock.connect_ex((self.HOST,self.PORT))
self.vedio_sock.send(("Get_video").encode())
print(11111)
while 1:
# 接受TCP链接并返回(conn, addr),其中conn是新的套接字对象,可以用来接收和发送数据,addr是链接客户端的地址。
length = self.recv_size(16) #首先接收来自客户端发送的大小信息
print(length)
if (int(length))!=0: #若成功接收到大小信息,进一步再接收整张图片
stringData = self.recv_all(int(length))
#stringData = self.vedio_sock.recv((int(length)*16))
#print(stringData)
data = numpy.fromstring(stringData, dtype='uint8')
decimg=cv2.imdecode(data,cv2.IMREAD_COLOR) #解码处理,返回mat图片
img_array = numpy.array(decimg, dtype=numpy.uint8)
img = Image.fromarray(img_array)
img_tk = ImageTk.PhotoImage(img)
#print(img_tk)
self.vedio_canvas.create_image(0,0,anchor = 'nw',image = img_tk)
self.root.update()
self.root.after(1)
if cv2.waitKey(10) == 27:
break
print('Image recieved successfully!')
self.vedio_sock.send(("Server has recieved messages!").encode())
if cv2.waitKey(10) == 27:
break
self.vedio_sock.close()
cv2.destroyAllWindows()

if __name__ == '__main__':
chat_ui = ChatUI()
print("sddddd")
data='单击连接以连接服务器\n'
chat_ui.text.insert(tk.END, data)
chat_ui.root.mainloop()

中间一些奇怪的打表请忽略(

服务器代码改善

服务器端的代码还是以服务器通信这里的为蓝本改的。
主要是将各个部分分开,然后主逻辑增添video连接的选项,client类添加了一下视频处理的代码。
写的时候在处理编码的地方耗了很久,python的格式感觉真的好乱,啥时候有时间整个用c++做的

main.py
1
2
3
4
5
6
7

from Work import start_server

if __name__ == "__main__":
start_server(8881)#这里配一下你的端口
print("服务器已关闭")


Work.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

import socket,threading,time
from Client import client
clients = {}
def new_client_work(c,server):
try:
print("%s(%s) 尝试连接" %(c.addr,c.port))
data = c.recv()
if not data:
return
if len(data) >= 16:
c.socket.send("用户名太长了")
return
c.username = data
if data=='Get_video':
c.send_video(c.username)
return True
print("用户%s %s(%s)已连接" %(c.username,c.addr,c.port))
c.socket.send("已连接".encode("utf-8"))
while True:
data = c.recv()
if data=='--exit--':
broadcast(data,c.username)
break
if not data:
break
else:
print("用户%s %s(%s) 发送了: %s" % (c.username,c.addr, c.port, data))
broadcast(data,c.username)

except socket.errno as e:
print("Socket error: %s" % str(e))
except Exception as e:
print("Other exception: %s" % str(e))
finally:
print("%s(%s) 断开连接" % (c.addr, c.port))
c.close()
clients.pop(c.getId())

def broadcast(msg,username,admin=False):
for c in clients.values():
c.sendMsg(msg,username,admin)

def start_server(port):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

host = "127.0.0.1"
server.bind((host, port))

# 监听客户端
server.listen(10)
print("服务器已开启,正在监听{}".format(server.getsockname()))

while True:
# 接受客户端连接
conn, addr = server.accept()
c = client(conn,addr,"")
clients[c.getId()] = c
# 为该连接创建新线程
t = threading.Thread(target=new_client_work, args=(c,server))
t.start()


Client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from PIL import Image
import socket,threading,time
import cv2
import numpy
class client(object):
def __init__(self,socket,addr,username):
self.addr = addr[0]
self.port = addr[1]
self.username = username
self.socket=socket

def sendMsg(self,msg,username,admin):
try:
if admin:
self.socket.send(("%s %s(管理员): %s" % (self.getTime(), username, msg)).encode("utf-8"))
else:
self.socket.send(("%s %s: %s" %(self.getTime(), username, msg)).encode("utf-8"))
return True
except:
return False

def recv(self,mtu=1024):
try:
data = self.socket.recv(mtu).decode("utf-8")
if data == "-!-quit-!-" or not data:
return False
return data
except:
return False
def send_video(self,username):
try:
self.capture = cv2.VideoCapture(0)# 从摄像头采集图像
self.ret, self.frame = self.capture.read()
self.fps = self.capture.get(5)/10000 #查询帧率
self.encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90] #设置编码参数
#print(1111111111111111)
while self.ret:
#print(1111111111111111)
#cvimage = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGBA)
#pilImage=Image.fromarray(cvimage)
#self.frame = pilImage.resize((300, 250),Image.ANTIALIAS)
self.result, self.imgencode = cv2.imencode('.jpg', self.frame)
data = numpy.array(self.imgencode)
#print(data)
O_datas=numpy.array([])
stringData = data.tobytes()
datas=O_datas.tobytes()
#print(1111111111111111)
self.socket.send((str((len(stringData))).ljust(16)).encode())
for i in range (0,len(stringData)):
datas+=data[i]
if ((i+1)%4096==0)or (i+1==len(stringData)):
self.socket.send(datas)
datas=O_datas.tobytes()
#self.socket.send(data)
#print(1111111111111111)
self.ret, self.frame = self.capture.read()
print(self.fps)
iloop=self.fps/2 #每秒处理2帧
while True:
self.capture.grab() #只取帧不解码,
iloop =iloop - 1
if iloop <1 :
break
if cv2.waitKey(1) & 0xFF == ord('q'):
break
#print(1111111111111111)
data_r = self.socket.recv(50)
print(data_r)
except :
pass
finally:
self.capture.release()
self.close()

def close(self):
try:
self.socket.close()
cv2.destroyAllWindows()
return True
except:
return False

def getId(self):
return "%s-%s" % (self.addr,self.port)
def getTime(self):
return str(time.strftime("%Y-%m-%d %H:%M:%S"))


一些奇怪的打表用于测试的请忽略(

成果

大概做完之后就是这样子的:
界面是这样的
运行后出来tkinter界面,然后点击视频按钮就可以获得服务器传来的实时摄像头视频。
(其实理论上应该是设计先用户登录,然后再点击视频按钮,这样子视频的连接就能和现在登录的这个用户的连接绑定,可以增加更多交互的可能性,不过有点赶工就没写这块逻辑)
目前的应用场景(在寝室直接监控机房)(雾)
偷窥!
(这样不好)
最后还有一点在做完之后才发现,这玩意好费流量啊,我就打开了大概一分钟不到的时间就用俩70多M,之前做纯文本的服务器通信七八天用不了100kb。
半分钟!!!
想了能不能图片压缩什么的减少通信量,不过现在的照出来已经蛮模糊的了就没怎么搞。
总之是当不了机房监控了(算一算一个月免费的内网穿透流量大概只能用仨四小时?)。可能等之后有自己的服务器和域名的话就不用受这限制。

总之是蛮好玩的,并且也会继续好玩下去。
呢就不好玩了


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!