UWP开发之StreamSocket聊天室(二)

Cle****-he UID.1073626
2015-11-05 发表

本帖最后由 entty 于 2015-11-19 00:48 编辑

本节主要知识点:
1.***链接停止解析***
2.***链接停止解析***
3.***链接停止解析***(Json.Net 第三方库)
4.***链接停止解析***
5.***链接停止解析***

这篇博客我们接着上次的说,今天来实现SocketBusiness项目里面的代码

SocketBusiness 主要用来处理StreamSocket客户端和服务端的逻辑的,先看下UML图大致了解下类关系:
***附件停止解析***

SocketBusiness中有四个类:

Quote SocketFactory :用来根据条件生成 客户端/服务端 ,简单工厂
SocketBase : Socket客户端/服务端的抽象类 ,包含 客户端/服务端 通用的一些方法
ClientSocket:客户端Socket类,继承与SocketBase
ServerSocket:服务端Socket类,继承与SocketBase


有人或许疑惑为什么要这样设计,其实是我偷懒了,为什么这么说?因为前不久刚做过一个小项目是客户端和服务端同体的App,我直接把SocketBusiness直接拿过来用了,何为客户端和服务端同体的App,就是说任何一台机器的App都有可能为服务端或者客户端,为了达到代码的复用性,所以就变成上面这种类关系了,由SocketBase提供抽象方法(启动服务端监听/连接服务端、关闭服务端监听/关闭客户端连接)以及统一的发送消息处理消息的逻辑,而像连接服务端/启动监听这种操作交由具体的客户端类和服务端类去实现。

而在我们的聊天室Demo中其实没必要这样做,我们完全可以将客户端ClientSocket的代码放到客户端项目中、ServerSocket的代码放到服务端项目中

但懒得改了,这样做虽然体现不出来什么好处,但绝对也没啥坏处。

那我们来看代码,注释写的很详细的我就不解释了

SocketBase
[mw_shl_code=csharp,true]namespace SocketBusiness
{
/// <summary>
/// Socket客户端/服务端 工厂
/// </summary>
public class SocketFactory
{
public static SocketBase CreatInkSocket(bool isServer, string ip, string serviceName)
{
return isServer ? (SocketBase) new ServerSocket(ip, serviceName) : new ClientSocket(ip, serviceName);
}
}


public abstract class SocketBase
{
/// <summary>
/// 客户端列表
/// </summary>
protected List<StreamSocket> ClientSockets;
/// <summary>
/// 用于连接或监听的ip地址
/// </summary>
protected string IpAddress;

/// <summary>
/// 标识是否为服务端
/// </summary>
protected bool IsServer;

/// <summary>
/// 新消息到达通知
/// </summary>
public Action<MessageModel> MsgReceivedAction;

/// <summary>
/// 服务端启动监听/客户端启动连接 失败时的通知
/// </summary>
public Action<Exception> OnStartFailed;

/// <summary>
/// 服务端启动监听/客户端启动连接 成功时的通知
/// </summary>
public Action OnStartSuccess;

/// <summary>
/// 连接或监听的端口号
/// </summary>
protected string RemoteServiceName;

/// <summary>
/// 客户端Socket对象
/// </summary>
protected StreamSocket Socket;

/// <summary>
/// 是否在监听端口/是否和服务端在保持着连接
/// </summary>
public bool Working;

/// <summary>
/// 客户端/服务端 流写入器
/// </summary>
protected DataWriter Writer;

/// <summary>
/// 客户端连接到服务器 / 服务端启动监听
/// </summary>
/// <returns></returns>
public abstract Task Start();

/// <summary>
/// 客户端断开连接 / 服务端停止监听
/// </summary>
public abstract void Dispose();

/// <summary>
/// 发送消息
/// </summary>
/// <param name="msg">消息对象</param>
/// <param name="client">客户端Client对象</param>
/// <returns></returns>
public async Task SendMsg(MessageModel msg, StreamSocket client = null)
{
if (msg != null)
{
await SendData(client, JsonConvert.SerializeObject(msg));
}
}

protected async Task SendData(StreamSocket client, string data)
{
try
{
if (!Working) return;
if (string.IsNullOrEmpty(data)) return;

if (!IsServer)
{
if (Writer == null)
{
Writer = new DataWriter(Socket.OutputStream);
}
await WriterData(data);
}

else if (IsServer)
{
foreach (var clientSocket in ClientSockets.Where(s => s != client))
{
try
{
Writer = new DataWriter(clientSocket.OutputStream);
await WriterData(data);
//分离流 防止OutputStream对象被释放
Writer.DetachStream();
}
catch (Exception)
{
//
}
}
}
}
catch (Exception exception)
{
if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown)
{
throw;
}

Debug.WriteLine("Send failed with error: " + exception.Message);
}
}

private async Task WriterData(string data)
{
//转成 byte[] 发送
var bytes = Encoding.UTF8.GetBytes(data);
//先写入数据的长度
Writer.WriteInt32(bytes.Length);
//写入数据
Writer.WriteBytes(bytes);
await Writer.StoreAsync();
Debug.WriteLine("Data sent successfully.");
}
}
}[/mw_shl_code]

SocketBase.cs中的代码主要是定义服务端/客户端的抽象方法以及实现发送数据的逻辑

无论是服务端还是客户端,如果想要发送数据到远程目标计算机,都需先拿到对方的Socket对象的OutputStream流,然后使用DataWriter对象的各种Write方法写入数据,最后通过调用DataWriter对象的StoreAsync异步发送数据。

DataWriter支持写入很多种数据,具体如下:
[mw_shl_code=csharp,true]// 将布尔值写入输出流。
public void WriteBoolean(System.Boolean value);

// 将指定缓冲区的内容写入输出流。
public void WriteBuffer(IBuffer buffer);

// 从缓冲区的指定字节写入输出流。
public void WriteBuffer(IBuffer buffer, System.UInt32 start, System.UInt32 count);

// 将字节值写入输出流。
public void WriteByte(System.Byte value);

// 将一个字节值数组写入到输出流。
public void WriteBytes(System.Byte[] value);

// 将DateTime写入到输出流。
public void WriteDateTime(DateTimeOffset value);

// 将浮点值写入输出流。
public void WriteDouble(System.Double value);

// 将 GUID 值写入输出流。
public void WriteGuid(Guid value);

// 将 16 位整数值写入输出流。
public void WriteInt16(System.Int16 value);

// 将 32 位整数值写入输出流。
public void WriteInt32(System.Int32 value);

// 将 64 位整数值写入输出流。
public void WriteInt64(System.Int64 value);

// 将浮点值写入输出流。
public void WriteSingle(System.Single value);

// 将字符串值写入输出流。
public System.UInt32 WriteString(System.String value);

// 将TimeSpan写入输出流。
public void WriteTimeSpan(TimeSpan value);

// 将 16位无符号整数值写入输出流。
public void WriteUInt16(System.UInt16 value);

// 将 32 位无符号整数值写入输出流。
public void WriteUInt32(System.UInt32 value);

// 将 64 位无符号整数值写入输出流。
public void WriteUInt64(System.UInt64 value);[/mw_shl_code]

不过我建议发送数据时,将数据的Model进行json或者xml序列化为string,然后再将数据string转为byte[] 作为最终数据在网络中传输。

ServerSocket
先贴下完整代码:
[mw_shl_code=csharp,true]public class ServerSocket : SocketBase
{
/// <summary>
/// Socket监听器
/// </summary>
protected StreamSocketListener Listener;

/// <summary>
/// 构造函数
/// </summary>
/// <param name="ip">ip</param>
/// <param name="remoteServiceName">端口号</param>
public ServerSocket(string ip, string remoteServiceName)
{
IpAddress = ip;
RemoteServiceName = remoteServiceName;
}

/// <summary>
/// 启动监听
/// </summary>
/// <returns></returns>
public override async Task Start()
{
try
{
if (Working) return;
IsServer = true;
ClientSockets = new List<StreamSocket>();
Listener = new StreamSocketListener()
{
Control = { KeepAlive = false }
};
Listener.ConnectionReceived += OnConnection; //新连接接入时的事件
await Listener.BindEndpointAsync(new HostName(IpAddress), RemoteServiceName);
Working = true;
OnStartSuccess?.Invoke();
}
catch (Exception exc)
{
OnStartFailed?.Invoke(exc);
}
}

private async void OnConnection(StreamSocketListener sender,
StreamSocketListenerConnectionReceivedEventArgs args)
{
Writer = null;
//获取新接入的Socket的InputStream 来读取远程目标发来的数据
var reader = new DataReader(args.Socket.InputStream);

//添加一个StreamSocket客户端到 客户端列表
ClientSockets.Add(args.Socket);
try
{
while (Working)
{
//等待数据进来
var sizeFieldCount = await reader.LoadAsync(sizeof(uint));
if (sizeFieldCount != sizeof(uint))
{
reader.DetachStream();
reader.Dispose();
//主动断开连接
ClientSockets?.Remove(args.Socket);
return;
}

var stringLength = reader.ReadUInt32();
//先获取数据的长度
var actualStringLength = await reader.LoadAsync(stringLength);
if (stringLength != actualStringLength)
{
//数据接收中断开连接
reader.DetachStream();
reader.Dispose();
ClientSockets?.Remove(args.Socket);
return;
}

var dataArray = new byte[actualStringLength];
//根据数据长度获取数据
reader.ReadBytes(dataArray);
//转为json数据字符串
var dataJson = Encoding.UTF8.GetString(dataArray);
//反序列化数据为对象
var data = JsonConvert.DeserializeObject<MessageModel>(dataJson);
//给所有客户端发送数据
await SendMsg(data, args.Socket);
//触发新消息到达Action
MsgReceivedAction?.Invoke(data);
}
}
catch (Exception exception)
{
if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown)
{
//
}
Debug.WriteLine(string.Format("Received data: \"{0}\"",
"Read stream failed with error: " + exception.Message));
reader.DetachStream();
reader.Dispose();
ClientSockets?.Remove(args.Socket);
}
}

public async override void Dispose()
{
Working = false;
//给所有客户端发送断开服务的消息
await SendMsg(new MessageModel
{
MessageType = MessageType.Disconnect
});
foreach (var clientSocket in ClientSockets)
{
clientSocket.Dispose();
}
ClientSockets.Clear();
ClientSockets = null;

Listener.ConnectionReceived -= OnConnection;
Listener?.CancelIOAsync();
Listener.Dispose();
Listener = null;
}
}[/mw_shl_code]

服务端启动监听的步骤:
[list=1]
[*]创建StreamSocketListener监听对象
[*]订阅SteamSocketListener对象的ConnectionReceived事件
[*]调用SteamSocketListener对象的BindEndpointAsync方法来启动Socket监听
[/list]
主要代码片段:
[mw_shl_code=csharp,true]Listener = new StreamSocketListener()
{
Control = { KeepAlive = false }
};
Listener.ConnectionReceived += OnConnection; //新连接接入时的事件
await Listener.BindEndpointAsync(new HostName(IpAddress), RemoteServiceName);[/mw_shl_code]

敬告:
为防止不可控的内容风险,本站已关闭新用户注册,新贴的发表及评论;
你现在看到的内容只是互联网用户曾经发表的言论快照,仅用于老用户留存纪念,且仅与科技行业相关,全部内容不代表本站观点及立场;
本站重新开放前已针对包括用户隐私、版权保护、信息安全、国家政策在内的各种互联网法律法规要求,执行了隐患内容的自查、屏蔽和删除;
本站目前所属个人主体,未有任何盈利安排与计划,且与原WFUN.COM所属公司不存在任何关联关系;
如果本帖内容或者相关资源侵犯到您的合法权益,或者您认为存在问题,那么请您务必点此举报或投诉!
全部回复:
Cle****-he UID.1073626
2015-11-05 回复

本帖最后由 entty 于 2015-11-19 00:47 编辑

UWP开发之StreamSocket聊天室(二) 续


当有客户端Socket请求连接服务端Socket时,就会触发StreamSocketListener监听器的ConnectionReceived事件,在事件中,我们通过事件StreamSocketListenerConnectionReceivedEventArgs参数可以拿到客户端的socket对象

然后使用客户端的Socket.InputStream 来初始化 DataReader对象,使用DataReader来循环读取数据流即可。

作为聊天室的服务端我们除了要处理每个客户端的数据外,我们在接受到每个客户端的消息时还要把该消息转发到其他客户端,所以在接受到数据后要坐下客户端群转发数据。

主要代码片段:
[mw_shl_code=csharp,true]var dataArray = new byte[actualStringLength];
//根据数据长度获取数据
reader.ReadBytes(dataArray);
//转为json数据字符串
var dataJson = Encoding.UTF8.GetString(dataArray);
//反序列化数据为对象
var data = JsonConvert.DeserializeObject<MessageModel>(dataJson);
//给所有客户端发送数据
await SendMsg(data, args.Socket);
//触发新消息到达Action
MsgReceivedAction?.Invoke(data);[/mw_shl_code]

ClientSocket

[mw_shl_code=csharp,true]public class ClientSocket : SocketBase
{
private HostName _hostName;

/// <summary>
/// 构造函数
/// </summary>
/// <param name="ip">目标ip</param>
/// <param name="remoteServiceName">端口号</param>
public ClientSocket(string ip, string remoteServiceName)
{
IsServer = false;
IpAddress = ip;
RemoteServiceName = remoteServiceName;
}

/// <summary>
/// 开始连接到服务端
/// </summary>
/// <returns></returns>
public override async Task Start()
{
try
{
if (Working) return;
_hostName = new HostName(IpAddress);
//初始化StreamSocket对象
Socket = new StreamSocket();
Socket.Control.KeepAlive = false;

Debug.WriteLine(("Connecting to: " + _hostName.DisplayName));
//开始连接目标计算机
await Socket.ConnectAsync(_hostName, RemoteServiceName);
OnStartSuccess?.Invoke();
Debug.WriteLine("Connected");
Working = true;
await Task.Run(async () =>
{
//创建一个读取器 来读取服务端发送来的数据
var reader = new DataReader(Socket.InputStream);

try
{
while (Working)
{
var sizeFieldCount = await reader.LoadAsync(sizeof(uint));
if (sizeFieldCount != sizeof(uint))
{
//主动断开连接
reader.DetachStream();
OnStartFailed?.Invoke(new Exception("断开连接"));
Dispose();
return;
}

var stringLength = reader.ReadUInt32();
var actualStringLength = await reader.LoadAsync(stringLength);
if (stringLength != actualStringLength)
{
//数据接收中断开连接
reader.DetachStream();
OnStartFailed?.Invoke(new Exception("断开连接"));
Dispose();
return;
}
//接受数据
var dataArray = new byte[actualStringLength];
reader.ReadBytes(dataArray);
//转为json字符串
var dataJson = Encoding.UTF8.GetString(dataArray);
//反序列化为数据对象
var data = JsonConvert.DeserializeObject<MessageModel>(dataJson);
//新消息到达通知
MsgReceivedAction?.Invoke(data);
}
}
catch (Exception exception)
{
if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown)
{
}
Debug.WriteLine(string.Format("Received data: \"{0}\"",
"Read stream failed with error: " + exception.Message));
reader.DetachStream();
OnStartFailed?.Invoke(exception);
Dispose();
}
});
}
catch (Exception exception)
{
if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown)
{
}
Debug.WriteLine(string.Format("Received data: \"{0}\"",
"Read stream failed with error: " + exception.Message));
OnStartFailed?.Invoke(exception);
Dispose();
}
}

public override void Dispose()
{
Working = false;
Writer = null;
Socket?.Dispose();
Socket = null;
}
}[/mw_shl_code]

客户端代码和服务端代码类似,唯一不同的就是需要使用StreamSocket对象的ConnectAsync方法来连接服务端计算机,连接成功后的操作和服务端的操作就一样了,也是使用DataReader对象来读取目标计算机发送来的数据

Ok,今天就到这吧,下篇来写服务端UI和界面逻辑的实现。

//为了方便智友查看,附上其它目录地址
***链接停止解析***
***链接停止解析***
***链接停止解析***
***链接停止解析***
***链接停止解析***


本文出自:53078485群大咖Aran

wanguangzhi UID.370925
2015-11-05 回复

不明觉厉

qiqiminmin UID.638527
2015-11-05 回复

{:5_214:}, 非常感谢分享。非常详细,楼主幸苦了。 使用socket.io的飘过。。。。。。。

前面的路 UID.932351
2015-11-05 使用 Lumia 920T 回复

支持开发者

adamziyun UID.992625
2015-11-05 使用 Lumia 830 回复

苍天,完全看不懂!

tmp00000 UID.995403
2015-11-06 回复

好文章要顶。等我闲下来,我会做一套vb.net版的代码。

楼****天 UID.1149255
2015-11-08 使用 Lumia 930 回复

看得懂一点点

本站使用Golang构建,点击此处申请开源鄂ICP备18029942号-4联系站长投诉/举报