목차
0 미션 개요
아키텍처 형태:
1> Hololens*2 PC는 mqtt를 사용하여 세 가지 사이에서 통신합니다.
2> PC에서 개발하고, 2개의 홀로렌즈로부터 mqtt 메시지를 수신하고, 이를 웹소켓 서버(휴대용 단말기)로 전달합니다.
3> PC의 MQTT+WEBSOCKET(임시 직접 전달).
목표: mqtt의 메시지가 있다고 가정하고 이 메시지를 WebSocket 서버로 전달합니다.
1 서버와의 다양한 지속적인 통신 방법 비교
>>>>>>>>>>>> | 전통적인 여론조사 | 긴 폴링 | 서버 전송 이벤트 | 웹소켓 |
---|---|---|---|---|
브라우저 지원 | 거의 모든 최신 브라우저 | 거의 모든 최신 브라우저 | 파이어폭스 6+ 크롬 6+ 사파리 5+ 오페라 10.1+ | IE 10+ Edge Firefox 4+ Chrome 4+ Safari 5+ Opera 11.5+ |
서버 부하 | 더 적은 CPU 리소스, 더 많은 메모리 리소스 및 대역폭 리소스 | 기존 폴링과 유사하지만 더 적은 대역폭을 사용합니다. | 각 요청이 전송된 후 서버 연결을 끊을 필요가 없다는 점을 제외하면 긴 폴링과 유사합니다. | 루프(장기 폴링)에서 기다릴 필요가 없으며 CPU 및 메모리 리소스는 클라이언트 수가 아닌 클라이언트 이벤트 수로 측정됩니다. 네 가지 방법 중 성능이 가장 좋습니다. |
클라이언트 로드 | 더 많은 메모리 리소스와 요청 수를 차지합니다. | 기존 여론조사와 유사합니다. | 브라우저에서 기본적으로 구현되며 리소스를 거의 차지하지 않습니다. | 동일한 서버 전송 이벤트입니다. |
지연 | 실시간이 아니며 대기 시간은 요청 간격에 따라 다릅니다. | 기존 여론조사와 동일합니다. | 비실시간, 기본 지연은 3초이며 지연은 사용자 정의할 수 있습니다. | 실시간. |
구현 복잡성 | 매우 간단합니다. | 서버 협력이 필요하며 클라이언트 구현은 매우 간단합니다. | 서버 협력이 필요하며 클라이언트 구현은 처음 두 가지보다 훨씬 간단합니다. | 소켓 프로그램 구현 및 추가 포트가 필요하며 클라이언트 구현이 간단합니다. |
웹소켓에 대한 자세한 설명
http - WebSocket에 대한 자세한 설명 - 까다롭지 않은 프로그래머 - SegmentFault SiFou
2 C++ 참조 링크:
zaphoyd/websocketpp: C++ 웹소켓 클라이언트/서버 라이브러리(github.com)
컴퓨터에서 두 개의 cpp 파일인 example/echo_client 및 echo_server를 실행해 보십시오. 다른 파일을 시도했지만 실행에 실패했습니다.
결과: 연결에 성공했지만 사용자 정의된 메시지를 보낼 수 없습니다.
서버 스크린샷:
클라이언트 스크린샷:
다른 블로그 포스팅에서는 mqtt 정보를 받기 위해 C#을 사용했는데, 개발을 위해 C#으로 바꾸는 것을 고려했습니다.
링크: https://mp.csdn.net/mp_blog/creation/editor/new/129476972
3 C# 구현
서버 코드:
using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WebSocketServer
{
class WebSocket_Server
{
#region 避免重复显示
static string lastMsg;
static int lastCount;
static int dupMsgCount = 0;
static bool isFirst = true;
#endregion
static async Task Main(string[] args)
{
string localIp = "127.0.0.1"; // 默认本地IP地址
string port = ":8080";
foreach (IPAddress ip in Dns.GetHostAddresses(Dns.GetHostName()))
{
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
localIp = ip.ToString();
break;
}
}
Console.WriteLine("Server started at: " + localIp + port);
HttpListener listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
listener.Start();
while (true)
{
HttpListenerContext context = await listener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
WebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
WebSocket webSocket = webSocketContext.WebSocket;
Console.WriteLine("Client connected: " + context.Request.RemoteEndPoint.ToString());
await Echo(webSocket, context);
}
else
{
context.Response.StatusCode = 400;
context.Response.Close();
}
}
}
private static async Task Echo(WebSocket webSocket, HttpListenerContext context)
{
byte[] buffer = new byte[1024];
while (webSocket.State == WebSocketState.Open)
try
{
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
if (isFirst)
{
lastMsg = message; //将第一次进入函数体的消息初始化为message的内容
lastCount = 1;
isFirst = false;
}
//Console.WriteLine("Received: message - " + message);
//如果收到的信息重复,则不显示
if (string.Equals(lastMsg, message))
{
dupMsgCount++;
}
else if ((dupMsgCount != 1) && (dupMsgCount != 0))
{
lastCount = dupMsgCount;
//Console.WriteLine("Received: response - " + response + " " + dupMsgCount + " times. ");
Console.WriteLine("Received: message - " + lastMsg + " " + lastCount + " times. ");
dupMsgCount = 1;
}
lastMsg = message;
byte[] responseBuffer = Encoding.UTF8.GetBytes("Echo: " + message);//将收到的数据回响给客户端
await webSocket.SendAsync(new ArraySegment<byte>(responseBuffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
Console.WriteLine("Client disconnected: " + context.Request.RemoteEndPoint.ToString());
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
}
}
클라이언트 코드:
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WebSocketClient
{
class WebSocket_Client
{
static async Task Main(string[] args)
{
ClientWebSocket webSocket = new ClientWebSocket();
string targetURI = "ws://localhost:8080";
await webSocket.ConnectAsync(new Uri(targetURI), CancellationToken.None);
Console.WriteLine("Connected to server at: " + targetURI);
byte[] buffer;
string message;
while (true)
{
Console.Write("Enter a message (type 'exit' to quit): ");
message = Console.ReadLine();
if (message.ToLower() == "exit")
{
break;
}
buffer = Encoding.UTF8.GetBytes(message);
await webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
buffer = new byte[1024];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
string response = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine("Received: " + response);
}
// Send a close message to the server before closing the connection
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
}
}
4개의 코드 통합
MQTT+WebSocket 클라이언트
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;
using System.IO;
namespace csharpMQTT
{
class CsharpMQTT_Com
{
static string _msg;
#region 避免重复显示
static string lastMsg;
static int lastCount;
static int dupMsgCount = 0;
static bool isFirst = true;
#endregion
//MQTT部分
static MqttClient ConnectMQTT(string broker, int port, string clientId, string username, string password)
{
MqttClient client = new MqttClient(broker, port, false, MqttSslProtocols.None, null, null);
client.Connect(clientId, username, password);
if (client.IsConnected)
{
Console.WriteLine("Connected to MQTT Broker. Client Id: " + clientId.ToString());
}
else
{
Console.WriteLine("Failed to connect");
}
return client;
}
static async Task Publish(MqttClient client, string topic)
{
int msg_count = 0;//自定义消息的内容
while (true)
{
//System.Threading.Thread.Sleep(1 * 1000);
await Task.Delay(TimeSpan.FromSeconds(1));
string msg = "messages: " + msg_count.ToString();
client.Publish(topic, System.Text.Encoding.UTF8.GetBytes(msg));//将msg通过话题topic发布
Console.WriteLine("Send `{0}` to topic `{1}`", msg, topic);
msg_count++;
_msg = msg;
_ = WebSocketClient_Main();
}
}
static void Subscribe(MqttClient client, string topic)
{
client.MqttMsgPublishReceived += client_MqttMsgPublishReceived;//用于将clientMqttMsgPublishReceived方法添加到对象MqttMsgPublishReceived的事件中。当客户端接收到来自订阅主题的消息时,将调用clientMqttMsgPublishReceived方法。
client.Subscribe(new string[] { topic }, new byte[] { MqttMsgBase.QOS_LEVEL_AT_MOST_ONCE });
}
static void client_MqttMsgPublishReceived(object sender, MqttMsgPublishEventArgs e)
{
//sender是事件的发送者,即MqttClient对象,e是事件参数,即MqttMsgPublishEventArgs对象。
//MqttMsgPublishEventArgs对象包含有关消息的信息:主题、内容、是否重复、质量等级、是否应被保留。
string payload = System.Text.Encoding.Default.GetString(e.Message);
Console.WriteLine("Received `{0}` from `{1}` topic", payload, e.Topic.ToString());
}
static async Task Main()
{
try
{
string broker = "broker.emqx.io";//默认的云端服务器地址
//string broker = "115.156.168.35"; //本地服务器ip
int port = 1883;
string topic = "Csharp/mqtt";
string clientId = Guid.NewGuid().ToString();//生成全局唯一标识符,用于标识客户端ID地址
string username = "emqx";
string password = "public";
MqttClient client = ConnectMQTT(broker, port, clientId, username, password);
Subscribe(client, topic);
//异步方法,它不会阻塞当前线程。相反,它会返回一个Task对象,表示指定的时间过去后,操作将完成。在等待这个Task对象完成的过程中,线程可以执行其他操作。这使得程序更加响应,因为用户可以与程序进行交互,而不必等待Task.Delay返回。
var cancellationTokenSource = new CancellationTokenSource();
var publishTask = Task.Run(() => Publish(client, topic), cancellationTokenSource.Token);
await publishTask;
//Publish(client, topic);
}
catch (Exception ex)
{
Console.WriteLine("An error occurred: " + ex.Message);
}
}
//WebSocket_Client部分
static async Task WebSocketClient_Main()
{
//连接WebSocket服务器
ClientWebSocket webSocket = new ClientWebSocket();
string targetURI = "ws://localhost:8080";
await webSocket.ConnectAsync(new Uri(targetURI), CancellationToken.None);
Console.WriteLine("Connected to server at: " + targetURI);
//发送和接收消息
byte[] buffer;
string message;
while (true)
{
#region 注释的是原先手动输入信息内容的代码
//Console.Write("Enter a message (type 'exit' to quit): ");
//message = Console.ReadLine();
//if (message.ToLower() == "exit")
//{
// break;
//}
#endregion
message = _msg;//将消息替换为mqtt处的消息
if (isFirst)
{
lastMsg = message; //将第一次进入函数体的消息初始化为message的内容
lastCount = 1;
isFirst = false;
}
buffer = Encoding.UTF8.GetBytes(message);
await webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
buffer = new byte[1024];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
string response = Encoding.UTF8.GetString(buffer, 0, result.Count);//将从服务器端收到的数据存储
//如果收到的信息重复,则不显示
if (string.Equals(lastMsg, message))
{
dupMsgCount++;
}
else if ((dupMsgCount != 1) && (dupMsgCount != 0))
{
lastCount = dupMsgCount;
//Console.WriteLine("Received: response - " + response + " " + dupMsgCount + " times. ");
Console.WriteLine("Received: response - " + lastMsg + " " + lastCount + " times. ");//记录数据改变时,实际上已经是下一组数据了;所以要记录改变前的数据和次数,对外输出。
dupMsgCount = 1;
}
lastMsg = message;
}
//关闭WebSocket连接
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
}
}
5 테스트 과정
1> mqtt 서버 시작
20230523 emqx-5.0.25-windows-amd64.zip을 다운로드하고 위와 같은 방법으로 실행하면 경로에 문제가 있다는 메시지가 뜹니다. 아래 그림과 같이
솔루션을 찾을 수 없습니다. emqx-4.4.18-otp24.3.4.6-windows-amd64가 대신 사용되었습니다.
DOS 창을 실행하려면 bin 디렉터리를 입력하고 emqx start를 입력합니다. 4.x 버전은 프롬프트 없이 성공적으로 실행됩니다(5.x 버전에는 두 줄의 코드가 있습니다). 작업 관리자에 다음과 같이 추가 Erlang 항목이 있습니다. . 그것은 성공이다.
2> WebSocket_Server 시작
클라이언트 연결 인터페이스를 기다리는 중:
3> WebSocket_Client 시작
클라이언트가 성공적으로 시작되고 연결됩니다.
4> 서버정보
성공적인 연결 후 서버 인터페이스
클라이언트가 예기치 않게 종료되면 서버 연결이 끊어지지 않습니다.
6> 이후 그림과 같이 콘솔 출력 정보 통계를 만들기 위해 일부 내용이 수정되었습니다.
6 보충 지식
1.Mqtt 메시지 품질 수준
SpringBoot가 개발한 MQTT 프로토콜 메시지 품질 수준 분석_알면 알수록 모르는 것이 많다 블로그-CSDN 블로그
qos는 게시자와 구독자 간의 계약이 아니라 발신자와 수신자 간의 계약입니다.
즉, 게시자가 qos = 1인 메시지를 게시하는 경우 브로커가 메시지를 한 번 이상 수신할 수 있다는 것만 보장할 수 있으며, 해당 구독자가 메시지를 한 번 이상 수신할 수 있는지 여부도 해당 메시지에 따라 다릅니다. 가입자 및 브로커가 협상한 qos 수준.
qos가 1 또는 2이면 메시지는 messageId를 전달하지만 qos가 0이면 그렇지 않습니다.
qos = 0, 최대 1회 전달. 즉, 메시지를 보낸 후 바로 삭제되므로 나중에는 아무 것도 할 수 없습니다.
qos = 1, 최소 한 번 전달됩니다. 송신자가 메시지를 보낸 후 메시지를 저장하고 messageId로 수신자의 응답을 기다리고, 응답이 없으면 응답이 도착할 때까지 메시지를 다시 보내고 삭제합니다.
qos = 2, 정확히 하나의 전달.1 메시지를 보낸 후 보낸 사람은 보낸 메시지를 저장하고 받는 사람의 응답을 기다리며, 응답이 없으면 다시 보냅니다.
2 수신자는 수신된 messageId를 기록하고 동일한 messageId를 가진 모든 후속 메시지는 삭제됩니다.
3 수신자는 messageId를 포함한 확인 메시지를 보내고 송신자의 응답을 기다린 후, 송신자가 응답하지 않으면 수신자는 확인 메시지를 반복해서 보냅니다.
4 발신자는 확인 메시지를 받은 후 보낸 메시지를 삭제하고 확인된 메시지를 messageId와 함께 보냅니다.
5 수신자는 확인 메시지를 수신한 후 확인 메시지를 삭제합니다.