1、网络通信概念
1.1、核心定义:
网络通信是两个或多个计算设备通过传输介质和通信协议进行数据交换的过程。
本质上是数字信号的传输与解析
1.2、基本要素:
通信节点:发送端和接收设备、
传输介质:有线(光纤/电话线)或无线(wifi/4G/5G)
通信协议:TCP/IP、HTTP、Websocket等协议
数据格式:JSON、XML等结构化数据
1.3、常用的通信方案:
1.3.1、HTTP通信:
基本特征:
请求-响应模型:客户端发起请求,服务端返回响应。
无状态协议:每个请求相互独立
基于TCP:默认端口80(http)或者443(https)
版本演进
HTTP/1.0:每个请求新建TCP连接
HTTP/1.1:引入持久连接(Keep-Alive) 支持管道化(pipelining)
HTTP/2:二进制分帧、多路复用、头部压缩
优点:简单易用、防火墙友好、丰富的工具链支持
缺点:单向通信模式、头部开销大、实时性较差
1.3.2、Socket通信:
定义:Socket是操作系统提供的底层API,用于实现不同主机之间进程通信,基于传输层协议(TCP、UDP)
特点:直接操作网络层,支持多种协议(TCP可靠传输、UDP无连接传输),适用于自定义协议的开发
1.3.3、WebSocket通信
定义:WebSocket是HTML5引入的应用层协议,通过HTTP升级机制建立持久连接,提供全双工通信能力。
特点:基于HTTP/HTTPS,以消息为单位传输数据,内置心跳机制(ping/pong),适用于浏览器与服务器的实时交互。
1.3.4、Socket与WebSocket核心差异
维度 | Socket通信 | WebSocket通信 |
协议层 | 传输层(TCP/UDP) | 应用层(基于HTTP升级) |
连接建立 | 直接通过IP+端口建立连接 | 需HTTP握手(Upgrade: websocket头) |
数据格式 | 字节流(需处理分包/粘包) | 消息帧(自动分帧,以消息为单位) |
通信模式 | 全双工(TCP)或单工(UDP) | 全双工(基于TCP) |
跨域支持 | 需手动处理(如CORS) | 默认支持跨域(同源策略下) |
适用环境 | 任意客户端/服务端(如C++、Python) | 主要浏览器环境,但也可用于服务端间通信 |
2.1、创建WebSocket服务端项目

2.2、引用nuget包,此处引用的是存储相关的包,WebSocket已经集成到dotnet webapi项目中。
<ItemGroup>
<PackageReference Include="CacheManager.SystemRuntimeCaching" Version="2.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.31" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
2.3、修改appsettings.json配置文件
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"SocketApi": {
"Server": "http://localhost:5072",
"SocketUrl": "/ws",
"OpenNoticeRecord": "true",
"HistoryNoticeCacheDay": "1",
"GroupAddress": "100001",
"AiringAddress": "public",
"OrientAddress": "orient"
},
"SocketAccount": [
{
"Account": "luoni",
"AccountName": "罗尼"
},
{
"Account": "kate",
"AccountName": "卡特"
}
],
"GroupSocketAccount": [
{
"GroupID": "100001",
"GroupName": "群聊1",
"GroupUsers": "luoni,kate"
}
],
"CacheType": "Redis",
"RedisConfig": {
"Connection": "localhost:6379",
"Db": "2"
}
}
2.3.1、创建群配置模型GroupSocketAccount.cs
namespace WS.SocketServer.Models
{
public class GroupSocketAccount
{
public string GroupID { get; set; }
public string GroupName { get; set; }
public string GroupUsers { get; set; }
}
}
2.4、创建消息处理相关操作类
2.4.1、创建消息处理中间件MySocketMiddleware.cs
using System.Net.WebSockets;
using System.Text;
namespace WS.SocketServer;
public class MySocketMiddleware
{
private readonly RequestDelegate _next;
private readonly ISocketServiceHandler _handler;
public MySocketMiddleware(RequestDelegate next, ISocketServiceHandler handler)
{
_next = next;
_handler = handler;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
var socket = await context.WebSockets.AcceptWebSocketAsync();
await _handler.OnConnectedAsync(socket);
try
{
var buffer = new byte[1024 * 1024 * 1];
while (socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
switch (result.MessageType)
{
case WebSocketMessageType.Text:
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
await _handler.ReceiveTextAsync(socket, message);
break;
case WebSocketMessageType.Binary:
await _handler.ReceiveFileAsync(socket, result, buffer);
break;
case WebSocketMessageType.Close:
_ = _handler.OnDisconnectedAsync(socket);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
catch (Exception e)
{
_ = _handler.OnDisconnectedAsync(socket);
var msg = $"处理Socket状态发生异常,Exception:{e}";
Console.WriteLine(msg);
}
}
else
{
await _next(context);
}
}
}
2.4.2、创建Socket对象管理接口ISocketServiceHandler
using System.Net.WebSockets;
namespace WS.SocketServer;
public abstract class ISocketServiceHandler
{
public abstract Task OnConnectedAsync(WebSocket socket);
public abstract Task OnDisconnectedAsync(WebSocket socket);
public abstract Task ReceiveTextAsync(WebSocket socket, string message);
public abstract Task ReceiveFileAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer);
public abstract Task SendMessageAsync(SingleMessageRequest request);
public abstract Task SendGroupMessageAsync(GroupMessageRequest request);
public abstract Task SendMassMessageAsync(MassMessageRequest request);
}
2.4.3、Socket连接对象实现类SocketServiceHandler
using Logger.Service.Interface;
using System.Net.Mail;
using System.Net.WebSockets;
using System.Security.Principal;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
using WS.SocketServer.Models;
using WS.SocketServer.Services.Dtos;
namespace WS.SocketServer;
public class SocketServiceHandler : ISocketServiceHandler
{
private readonly ISocketDataManager _sockets;
private readonly ILoggerContext _logger;
private readonly INoticeCacheManager _noticeCacheManager;
private readonly IConfiguration _configuration;
public SocketServiceHandler(ISocketDataManager sockets, ILoggerContext logger,
INoticeCacheManager noticeCacheManager,IConfiguration configuration)
{
_sockets = sockets;
_logger = logger;
_noticeCacheManager = noticeCacheManager;
_configuration = configuration;
}
public override async Task OnConnectedAsync(WebSocket socket)
{
try
{
var socketId = Guid.NewGuid().ToString("N");
await _sockets.AddSocket(socketId, socket);
}
catch (Exception e)
{
var logmsg = $"连接Socket异常:{e.Message}";
Console.WriteLine(logmsg);
_logger.Error(logmsg);
throw;
}
}
public override async Task OnDisconnectedAsync(WebSocket socket)
{
try
{
var socketId = _sockets.GetSocketId(socket);
if (socketId != null)
{
await SendCloConnectedNoticeAsync(socket,"");
await _sockets.RemoveSocketAsync(socketId);
}
}
catch (Exception e)
{
var logmsg = $"连接关闭,清除内存中的Socket对象发生异常:{e.Message}";
Console.WriteLine(logmsg);
}
}
public override async Task SendMessageAsync(SingleMessageRequest request)
{
try
{
var connection = _sockets.GetAllConnections().Where(it => it.Key == request.SocketId)
.Select(it => new
{
SocketId = it.Key,
Socket = it.Value
}).FirstOrDefault();
if (connection != null)
{
if (request.MessageType != MessageTypeEnum.Files)
{
await SendMessageAsync(connection.Socket, request.Message.ToString());
}
else
{
await SendFileMessageAsync(connection.Socket, (Stream)request.Message);
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
public override async Task SendGroupMessageAsync(GroupMessageRequest request)
{
try
{
var list = _sockets.GetAllConnections().Select(it => new
{
SocketId = it.Key,
Socket = it.Value
}).ToList();
if (request.SocketIds != null && request.SocketIds?.Length > 0)
{
list = list.Where(it => request.SocketIds.Contains(it.SocketId)).ToList();
}
if (list?.Count == 0)
{
throw new Exception("连接不存在");
}
if (request.MessageType != MessageTypeEnum.Files)
{
foreach (var connection in list)
{
await SendMessageAsync(connection.Socket, request.Message.ToString());
}
}
else
{
foreach (var connection in list)
{
await SendFileMessageAsync(connection.Socket, (Stream)request.Message);
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
public override async Task SendMassMessageAsync(MassMessageRequest request)
{
try
{
var list = _sockets.GetAllConnections()
.Where(w => w.Key != request.FromSocketId)
.Select(it => new
{
SocketId = it.Key,
Socket = it.Value
}).ToList();
if (list?.Count == 0)
{
throw new Exception("连接不存在");
}
if (request.Message == null)
{
throw new Exception("消息内容为空");
}
if (request.MessageType != MessageTypeEnum.Files)
{
foreach (var connection in list)
{
await SendMessageAsync(connection.Socket, request.Message.ToString());
}
}
else
{
foreach (var connection in list)
{
await SendFileMessageAsync(connection.Socket, (Stream)request.Message);
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private async Task SendFileMessageAsync(WebSocket socket, Stream stream)
{
if (socket.State != WebSocketState.Open)
{
throw new Exception("连接已关闭");
}
;
if (stream == null)
{
throw new Exception("附件数据不存在");
}
try
{
byte[] content = new byte[stream.Length];
await stream.ReadAsync(content, 0, content.Length);
stream.Seek(0, SeekOrigin.Begin);
await socket.SendAsync(new ArraySegment<byte>(content), WebSocketMessageType.Binary, true, CancellationToken.None);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private async Task SendMessageAsync(WebSocket socket, string message)
{
try
{
if (socket.State != WebSocketState.Open)
{
throw new Exception("连接已关闭");
}
;
byte[] content = Encoding.UTF8.GetBytes(message);
await socket.SendAsync(new ArraySegment<byte>(content), WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
public override async Task ReceiveTextAsync(WebSocket socket, string message)
{
var socketId = _sockets.GetSocketId(socket);
var macAddress = IPHelper.GetMacAddress();
if (!string.IsNullOrEmpty(socketId))
{
var messageDto = new WSMessageDto();
try
{
messageDto = JsonSerializer.Deserialize<WSMessageDto>(message);
if (messageDto?.Command == (int)MessageBodyCommandType.VerifyToken)
{
await GetValidateSuccessResponseAsync(socket, socketId, messageDto);
HandleSocketHistoryNoticesAsync($"{
MD5Helper
.GenerateMd5ByRequest(messageDto.FromAccount)}_{macAddress}", socket);
}
if (messageDto?.Command == (int)MessageBodyCommandType.Directional)
{
await HandleNoticeToOnlineSocketAsync(socket, messageDto);
}
if (messageDto?.Command == (int)MessageBodyCommandType.GroupChat)
{
await HandleNoticeToGroupSocketAsync(socket, messageDto);
}
if (messageDto?.Command == (int)MessageBodyCommandType.Broadcast)
{
await SendMassMessageAsync(new MassMessageRequest
{
Message = message,
FromSocketId = socketId,
MessageType = MessageTypeEnum.Text
});
}
}
catch (Exception e)
{
Console.WriteLine($"解析消息格式异常:{e.Message},{e}");
Console.WriteLine($"消息体格式:{message}");
await GetValidateExceptionResponse(socket, socketId);
}
}
}
public override async Task ReceiveFileAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer)
{
try
{
var socketId = _sockets.GetSocketId(socket);
var macAddress = IPHelper.GetMacAddress();
if (!string.IsNullOrEmpty(socketId) && buffer.Length > 0)
{
if (socketId.Contains($"_{macAddress}"))
{
var stream = new MemoryStream(buffer);
await SendMassMessageAsync(new MassMessageRequest
{
Message = stream,
FromSocketId = socketId,
MessageType = MessageTypeEnum.Files
});
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private async Task HandleNoticeToOnlineSocketAsync(WebSocket socket, WSMessageDto message)
{
* 从消息体中定位到发送方、接收方、操作指令,消息体中必须包含以下字段
* 第一种格式:认证消息
*{
"Command": 1001,
"CommandName": "登录认证",
"FromAccount": "luoni",
"FromAccountName": "罗尼·库尔曼",
"Content": ""
}
第二种格式:普通消息
{
"Command": 2001,
"CommandName": "普通消息",
"FromAccount": "qiao",
"FromAccountName": "乔·卡特",
"ToAccount": "luoni",
"ToAccountName": "罗尼·库尔曼",
"Content":"Hello 罗尼"
}
*/
var macAddress = IPHelper.GetMacAddress();
string dstAccount = message.ToAccount;
if (string.IsNullOrWhiteSpace(dstAccount))
{
throw new Exception("接收方为空...");
}
else
{
var recipientId = $"{
MD5Helper
.GenerateMd5ByRequest(dstAccount)}_{macAddress}";
var recipientWebSocket = _sockets.GetSocketById(recipientId);
if (recipientWebSocket == null)
{
var logMsg = $"接收人{dstAccount}不在线或未建立令牌";
Console.WriteLine(logMsg);
if (!string.IsNullOrWhiteSpace(dstAccount))
{
var cacheKey = $"{
MD5Helper
.GenerateMd5ByRequest(dstAccount)}_{macAddress}";
_noticeCacheManager.AddNotice(cacheKey, message);
await SendCloConnectedNoticeAsync(socket, dstAccount);
}
}
else
{
await SendMessageAsync(recipientWebSocket, message.Content);
var newKey = $"{DateTime.Now.ToString("yyyy-MM-dd")}:{dstAccount}";
_noticeCacheManager.AddNoticeRecord(newKey, message);
_logger.Debug("记录消息到Redis");
}
}
}
private async Task HandleNoticeToGroupSocketAsync(WebSocket socket, WSMessageDto message)
{
第三种格式:群发消息
{
"Command": 2002,
"CommandName": "群发消息",
"FromAccount": "luoni",
"FromAccountName": "罗尼·库尔曼",
"ToAccount": "100011101",
"ToAccountName": "测试群"",
"Content":"Hello 各位群友"
}
*/
List<GroupSocketAccount> groupUsers = _configuration.GetSection("GroupSocketAccount").Get<List<GroupSocketAccount>>();
var macAddress = IPHelper.GetMacAddress();
var toUsers = groupUsers?.Where(it => it.GroupID == message.ToAccount).FirstOrDefault()?.GroupUsers?.Split(",");
if (toUsers?.Length == 0)
{
throw new Exception("接收方为空...");
}
else
{
foreach (var dstAccount in toUsers)
{
var recipientId = $"{
MD5Helper
.GenerateMd5ByRequest(dstAccount)}_{macAddress}";
var recipientWebSocket = _sockets.GetSocketById(recipientId);
if (recipientWebSocket == null)
{
var logMsg = $"接收群组{dstAccount}不在线或没有相应的成员";
Console.WriteLine(logMsg);
if (!string.IsNullOrWhiteSpace(dstAccount))
{
var cacheKey = $"{
MD5Helper
.GenerateMd5ByRequest(dstAccount)}_{macAddress}";
_noticeCacheManager.AddNotice(cacheKey, message);
await SendCloConnectedNoticeAsync(socket, dstAccount);
}
}
else
{
await SendMessageAsync(recipientWebSocket, message.Content);
var newKey = $"{DateTime.Now.ToString("yyyy-MM-dd")}:{dstAccount}";
_noticeCacheManager.AddNoticeRecord(newKey, message);
_logger.Debug("记录消息到Redis");
}
}
}
}
private async Task GetValidateSuccessResponseAsync(WebSocket socket, string socketId, WSMessageDto message)
{
{
"Command": 1001,
"CommandName": "登录认证",
"FromAccount": "luoni",
"FromAccountName": "罗尼·库尔曼",
"Content": ""
}*/
var response = new UserTokenValidateResponse
{
Status = 200,
Message = "连接成功",
Token = socketId
};
await SendMessageAsync(socket, JsonSerializer.Serialize(response, new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
}));
var macAddress = IPHelper.GetMacAddress();
var recipientId = $"{
MD5Helper
.GenerateMd5ByRequest(message.FromAccount)}_{macAddress}";
await _sockets.EditSocketKey(socketId, recipientId);
var logMsg = $"建立连接成功,连接信息:SocketId:{socketId}";
Console.WriteLine(logMsg);
}
private async Task GetValidateExceptionResponse(WebSocket socket, string socketId)
{
var response = new UserTokenValidateResponse
{
Status = 401,
Message = "消息格式异常,无法发送,请参考文档"
};
await SendMessageAsync(socket, JsonSerializer.Serialize(response, new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
}));
await _sockets.RemoveSocketAsync(socketId);
var logMsg = $"建立连接失败,已清除Socket连接:SocketId:{socketId}";
Console.WriteLine(logMsg);
}
private void HandleSocketHistoryNoticesAsync(string cacheNoticeKey, WebSocket socket)
{
try
{
var historyNotices = _noticeCacheManager.GetUserAllHistoryNotices(cacheNoticeKey);
if (historyNotices != null)
{
foreach (var notice in historyNotices)
{
_ = SendMessageAsync(socket, notice.ToString());
}
var logMsg = $"建立连接成功,接收历史消息{historyNotices.Count}条";
Console.WriteLine(logMsg);
Console.WriteLine(cacheNoticeKey);
_noticeCacheManager.RemoveNotice(cacheNoticeKey);
}
}
catch (Exception e)
{
Console.WriteLine(e);
_logger.Error("推送离线消息异常:", e);
}
}
private async Task SendCloConnectedNoticeAsync(WebSocket socket, string toAccount)
{
try
{
var socketId = _sockets.GetSocketId(socket);
var noticeTemplate = $"用户{toAccount}已下线";
await SendMassMessageAsync(new MassMessageRequest
{
FromSocketId = socketId,
Message = noticeTemplate,
MessageType = MessageTypeEnum.Text
});
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
2.4.4、创建消息Command类型MessageBodyCommandType.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WS.SocketServer;
public enum MessageBodyCommandType
{
[Description("认证Token")]
VerifyToken = 1001,
[Description("定向单聊")]
Directional = 2001,
[Description("群聊")]
GroupChat = 2002,
[Description("广播")]
Broadcast = 2003
}
2.4.5、创建一个Socket对象管理接口ISocketDataManager,用于保存和管理Socket连接对象。
using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;
namespace WS.SocketServer;
public interface ISocketDataManager
{
ConcurrentDictionary<string, WebSocket> GetAllConnections();
WebSocket GetSocketById(string id);
string GetSocketId(WebSocket socket);
string GetSocketId(string account);
int GetCount();
Task RemoveSocketAsync(string id);
Task<bool> AddSocket(string socketId, WebSocket socket);
Task<bool> EditSocketKey(string oldSocketKey, string newSocketKey);
}
2.4.6、创建Socket连接对象管理的实现类SocketDataManager.cs
using System.Collections.Concurrent;
using System.Net.WebSockets;
namespace WS.SocketServer;
public class SocketDataManager : ISocketDataManager
{
private readonly ConcurrentDictionary<string, WebSocket> Connections = new();
public SocketDataManager()
{
}
public ConcurrentDictionary<string, WebSocket> GetAllConnections()
{
return Connections;
}
public WebSocket GetSocketById(string id)
{
var list = Connections;
return Connections.FirstOrDefault(x => x.Key == id).Value;
}
public string GetSocketId(WebSocket socket)
{
return Connections.FirstOrDefault(x => x.Value == socket).Key;
}
public string GetSocketId(string account)
{
var cacheKey = $"{
MD5Helper
.GenerateMd5ByRequest(account)}_verify";
return cacheKey;
}
public async Task RemoveSocketAsync(string id)
{
Connections.TryRemove(id, out var socket);
if (socket != null && socket?.State == WebSocketState.Open)
{
Console.WriteLine("socket连接关闭");
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "socket连接关闭", CancellationToken.None);
}
}
public async Task<bool> AddSocket(string socketId, WebSocket socket)
{
var result = Connections.TryAdd(socketId, socket);
return await Task.FromResult(result);
}
public async Task<bool> EditSocketKey(string oldSocketKey, string newSocketKey)
{
var result = false;
if (Connections.Any(it => it.Key == oldSocketKey))
{
Connections.TryRemove(oldSocketKey, out var socket1);
result = Connections.TryAdd(newSocketKey, socket1);
}
if (Connections.Any(it => it.Key == newSocketKey))
{
Console.WriteLine($"oldSocketKey:{oldSocketKey}, newSocketKey:{newSocketKey}");
var removeResult = Connections.TryRemove(oldSocketKey, out var socket2);
Connections.TryAdd(newSocketKey, socket2);
}
result = Connections.Any(it => it.Key == newSocketKey);
return await Task.FromResult(result);
}
public int GetCount()
{
return Connections.Count;
}
}
2.4.7、创建消息类型枚举MessageTypeEnum
using System.ComponentModel;
namespace WS.SocketServer
{
public enum MessageTypeEnum
{
[Description("文本")]
Text = 1,
[Description("文件流")]
Files = 2
}
}
2.4.8、创建消息手法的参数类MessageInfo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WS.SocketServer;
public class MessageInfo
{
public string NoticeId { get; set; }
public MessageTypeEnum MessageType { get; set; }
public object Message { get; set; }
}
3、创建缓存模块
3.1、缓存消息存储在Redis或者使用内存存储,历史消息使用Redis存储
3.2、创建CacheManager目录,并创建缓存接口ICacheContext.cs
namespace WS.SocketServer;
public interface ICacheContext
{
bool Exists(string key);
T GetCache<T>(string key);
bool SetCache<T>(string key, T value);
bool SetCache<T>(string key, T value, DateTime expire);
bool Remove(string key);
bool RenameKey(string key, string newKey);
}
3.3、创建缓存接口的内存实现
using System.Runtime.Caching;
namespace WS.SocketServer;
public class MemoryCacheService : ICacheContext
{
public bool Exists(string key)
{
return MemoryCache.Default.Contains(key);
}
public T GetCache<T>(string key)
{
return (T)MemoryCache.Default[key];
}
public bool SetCache<T>(string key, T value)
{
try
{
MemoryCache.Default.Set(key, value, new CacheItemPolicy
{
AbsoluteExpiration = DateTime.Now.AddYears(1)
});
return true;
}
catch (Exception e)
{
return false;
}
}
public bool SetCache<T>(string key, T value, DateTime expire)
{
try
{
MemoryCache.Default.Set(key, value, new CacheItemPolicy
{
AbsoluteExpiration = expire
});
return true;
}
catch (Exception e)
{
return false;
}
}
public bool Remove(string key)
{
try
{
MemoryCache.Default.Remove(key);
return true;
}
catch (Exception e)
{
return false;
}
}
public bool RenameKey(string key, string newKey)
{
try
{
MemoryCache.Default[key] = newKey;
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
}
}
3.4、创建缓存接口的Redis实现
using StackExchange.Redis;
using System.Text.Json;
namespace WS.SocketServer;
public class RedisCacheService : ICacheContext
{
private static ConnectionMultiplexer Conn { get; set; }
private static IDatabase RedisDb { get; set; }
public static void Init(string conn, int db = 0)
{
Conn = ConnectionMultiplexer.Connect(conn);
RedisDb = Conn.GetDatabase(db);
}
public RedisCacheService()
{
}
public bool Exists(string key)
{
return RedisDb.KeyExists(key);
}
public T GetCache<T>(string key)
{
RedisValue value = RedisDb.StringGet(key);
if (!value.HasValue)
return default;
if (typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(value, typeof(T));
}
else
{
return JsonSerializer.Deserialize<T>(value);
}
}
public bool SetCache<T>(string key, T value)
{
if (typeof(T) == typeof(string))
{
return RedisDb.StringSet(key, value?.ToString());
}
else
{
return RedisDb.StringSet(key, JsonSerializer.Serialize(value));
}
}
public bool SetCache<T>(string key, T value, DateTime expire)
{
if (typeof(T) == typeof(string))
{
return RedisDb.StringSet(key, value?.ToString(), expire - DateTime.Now);
}
else
{
return RedisDb.StringSet(key, JsonSerializer.Serialize(value), expire - DateTime.Now);
}
}
public bool Remove(string key)
{
return RedisDb.KeyDelete(key);
}
public bool RenameKey(string key, string newKey)
{
return RedisDb.KeyRename(key, newKey);
}
}
3.5、创建消息的缓存接口INoticeCacheManager.cs和实现类,用于管理离线消息
# INoticeCacheManager.cs
namespace WS.SocketServer;
public interface INoticeCacheManager
{
List<object> GetUserAllHistoryNotices(string toUser);
void AddNotice(string toUser, object message);
void AddNoticeRecord(string toUser, object message);
void RemoveNotice(string toUser);
}
#NoticeCacheHandler.cs
namespace WS.SocketServer;
public class NoticeCacheHandler : INoticeCacheManager
{
private readonly ICacheContext _cacheContext;
public NoticeCacheHandler(ICacheContext cacheContext)
{
_cacheContext = cacheContext;
}
public List<object> GetUserAllHistoryNotices(string toUser)
{
根据当前验证账号获取离线消息
//存储的离线消息针对接收方,当接收方上线后,Socket服务自动推送
以接收对象为Key,接收者上线后自动拉取
*/
try
{
var noticeAll = _cacheContext.GetCache<List<object>>(toUser);
return noticeAll;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public void AddNotice(string toUser, object message)
{
var noticeList = GetUserAllHistoryNotices(toUser);
if (noticeList != null)
{
noticeList.Add(message);
}
else
{
noticeList = new List<object>
{
message
};
}
_cacheContext.SetCache(toUser, noticeList);
}
public void AddNoticeRecord(string cacheKey, object message)
{
try
{
AddNotice(cacheKey, message);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public void RemoveNotice(string toUser)
{
try
{
_cacheContext.Remove(toUser);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
4.创建基础工具类
4.1、创建IP地址获取工具类IPHelper.cs,防止在不同IP登录后消息无法正常收发
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading.Tasks;
namespace WS.SocketServer;
public class IPHelper
{
public static string GetIp()
{
HttpContextAccessor _context = new HttpContextAccessor();
if (_context.HttpContext == null)
return string.Empty;
var ip = _context.HttpContext.Request.Headers["X-Forwarded-For"].ToString();
if (string.IsNullOrEmpty(ip))
{
ip = _context.HttpContext.Connection.RemoteIpAddress.ToString();
}
if (ip == "::1")
{
ip = "127.0.0.1";
}
return ip;
}
public static string GetMacAddress()
{
NetworkInterface[] networks = NetworkInterface.GetAllNetworkInterfaces();
if(networks.Length == 0)
{
return string.Empty;
}
var macAddress = string.Empty;
var effectiveNetworks = networks.Where(it=>it.NetworkInterfaceType == NetworkInterfaceType.Loopback && it.OperationalStatus == OperationalStatus.Up).ToList();
foreach (NetworkInterface adapter in effectiveNetworks)
{
PhysicalAddress address1 = adapter.GetPhysicalAddress();
macAddress = address1?.ToString();
if (string.IsNullOrWhiteSpace(macAddress))
{
IPInterfaceProperties properties = adapter.GetIPProperties();
var unicastAddress = properties.UnicastAddresses;
if(unicastAddress.Any(it=>it.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork))
{
var address2 = adapter.GetPhysicalAddress().ToString();
macAddress = address2?.ToString();
}
}
}
return macAddress;
}
}
4.2、创建MD5加密类MD5Helper.cs,用于格式化SocketID
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace WS.SocketServer;
public class MD5Helper
{
public static string GenerateNGuid()
{
return Guid.NewGuid().ToString("N");
}
public static string GenerateMd5ByRequest(string request)
{
using (var md5 = MD5.Create())
{
byte[] data = System.Text.Encoding.Default.GetBytes(request);
byte[] result = md5.ComputeHash(data);
string response = "";
for (int i = 0; i < result.Length; i++)
response += result[i].ToString("x").PadLeft(2, '0');
return response;
}
}
}
4.3、修改Program.cs,配置连接以及依赖注入
using Logger.Service;
using Logger.Service.Interface;
using WS.SocketServer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json", true).Build();
var cacheType = config.GetSection("CacheType").Value;
if (cacheType != "Redis")
{
builder.Services.AddSingleton<ICacheContext, MemoryCacheService>();
}
else
{
var redisConn = config.GetSection("RedisConfig:Connection").Value;
var redisDb = config.GetSection("RedisConfig:Db").Value;
builder.Services.AddSingleton<ICacheContext, RedisCacheService>();
RedisCacheService.Init(redisConn, Convert.ToInt32(redisDb));
}
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddTransient<INoticeCacheManager, NoticeCacheHandler>();
builder.Services.AddTransient<ISocketDataManager, SocketDataManager>();
builder.Services.AddTransient<ILoggerContext, NLogService>();
builder.Services.AddTransient<ISocketServiceHandler, SocketServiceHandler>();
var url = config.GetSection("SocketApi:Server").Value;
builder.WebHost.UseUrls(url);
builder.WebHost.UseKestrel(options =>
{
options.Limits.MaxRequestBodySize = int.MaxValue;
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(15),
ReceiveBufferSize = 1024 * 1024 * 10
});
var serviceProvider = app.Services;
var socketService = serviceProvider.GetService<ISocketServiceHandler>();
if (socketService != null)
{
var monitorUrl = config.GetSection("SocketApi:SocketUrl").Value;
app.Map(monitorUrl, it => it.UseMiddleware<MySocketMiddleware>(socketService));
}
app.UseAuthorization();
app.MapControllers();
app.Run();
5、测试WebSocket
5.1、用两台电脑分布打开https://wstool.js.org/
输入ws://localhost:5072/ws测试地址,点击开启连接,提示连接成功后可以开始测试

5.2、登录测试数据
{
"Command": 1001,
"CommandName": "登录认证",
"FromAccount": "luoni",
"FromAccountName": "罗尼·库尔曼",
"Content": ""
}
{
"Command": 1001,
"CommandName": "登录认证",
"FromAccount": "qiao",
"FromAccountName": "乔·卡特",
"Content":""
}
5.2.1、分别在两台机器上发送登录认证消息,等待正确返回

5.2.2、在另一个浏览器上执行另一个登录认证

5.3、发送文本消息
{
"Command": 2001,
"CommandName": "普通消息",
"FromAccount": "qiao",
"FromAccountName": "乔·卡特",
"ToAccount": "luoni",
"ToAccountName": "罗尼·库尔曼",
"Content":"Hello 罗尼"
}
{
"Command": 2001,
"CommandName": "普通消息",
"FromAccount": "luoni",
"FromAccountName": "罗尼·库尔曼",
"ToAccount": "kate",
"ToAccountName": "乔·卡特",
"Content":"Hello 乔"
}
5.3.1、从乔·卡特这个登录账号中发出消息
5.3.2、在另一个浏览器中接收到消息,则表示WebSocket通信已完成

5.4、发送群消息测试
{
"Command": 2002,
"CommandName": "群发消息",
"FromAccount": "luoni",
"FromAccountName": "罗尼·库尔曼",
"ToAccount": "100001",
"ToAccountName": "测试群",
"Content":"Hello 各位群友"
}
5.4.1、发送群聊消息,所有链接在线的群成员都可以收到,并且自己也会收到一份,也可以不将发送者配置到群里,具体可根据业务修改逻辑

WebSocket本身使用起来并不复杂,需要考虑到各种业务场景、以及性能损耗,比如针对一些超长文本的发送,数据传输和反序列化会消耗大量性能,可以使用文件流来传输,上面的代码中已经集成了文件流的传输,并且采用了分段式读取1M/频次。
当前案例只是提供了WebSocket服务端这块的功能,前端可以使用JavaScript或者其他一些集成库来测试。
本章节代码量过多,后期将会采用片段代码+在线源码的风格来编写此类文档,方便阅读和理解。
源码地址:
https://gitee.com/inc-zz/netCoreDemos/tree/master/12-WebSocket/src/WebSocketApi
阅读原文:原文链接
该文章在 2025/5/14 10:08:47 编辑过