名字
在第一篇的时候,我提到了我之后的工作中会经常性的使用winform(C# Windows窗体应用程序),所以在成功运行起HelloWorld的Server模块后,今天就开始着手于winform与tio服务端的通信建立。
在仔细阅读HelloWorld示例中的代码和老谭在提供的工程页面后,我们揪出三个重点:
1、消息包是由两部分组成的,消息包=消息长度+消息体。
2、消息体的编码在server端中的common模块中设定,HelloWorld示例中是设为了utf8编码。
3、消息长度在整个消息包的头部,且所占字节数由common模块中的HelloPacket类中设定,HelloWorld示例中消息长度设定了占4个字节。
针对这三个点,我们不妨设想一个包,比如我们要给服务端发送数字1,且消息我们用十六进制来表示。根据公式:消息包=消息长度+消息体,且消息体数字1在十六进制中表示为 0x31,且消息长度为1,用十六进制表示为0x01,补足4个字节,则为 0x00 0x00 0x00 0x01,最后:消息包=0x00 0x00 0x00 0x01 0x31 。
为了验证以上猜想的包是否正确,我去下载了一个tcp测试工具(tcpudp),通过链接上server后,通过16进制模式发送00 00 00 01 31包,果然服务端收到了消息1。
熟门熟路,打开宇宙第一IDE—-Visual Studio 2019(?),建立一个.Net4下的Winform,拖几个控件建立起界面,打开Nuget包管理器(可以理解为Java中的Maven的部分功能),引入SuperSocket.ClientEngine,下面就开始敲代码。
为了快速验证是否能发送,我直接所有的代码都在FormMain.cs中先敲出来,代码如下:
public partial class FormMain : Form
{
public FormMain()
{
InitializeComponent();
}
private AsyncTcpSession client;
private void button_Connect_Click(object sender, EventArgs e)
{
string regexIpPort =
@"((25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d))):(\d{1,})";
if (Regex.IsMatch(textBox_SeverIpAndPort.Text.Replace(":", ":"), regexIpPort
))
{
string[] ipPort = Regex.Split(textBox_SeverIpAndPort.Text.Trim(), ":");
client.Connect(new IPEndPoint(IPAddress.Parse(ipPort[0]), Convert.ToInt32(ipPort[1])));
}
}
void client_Error(object sender, ErrorEventArgs e)
{
SetConnectStatus($"发生错误,{e.Exception.Message}");
}
void client_Connected(object sender, EventArgs e)
{
this.button_ConnectOrDisConnect.Invoke(new MethodInvoker(() =>
{
this.button_ConnectOrDisConnect.Text = @"断开";
}));
SetConnectStatus("已连接");
}
//接收消息
void client_DataReceived(object sender, DataEventArgs e)
{
//接收到的消息必须大于消息长度头部所设定的字节长度
if (e.Length > 4)
{
byte[] newBytes = new byte[e.Length - 4];
newBytes = e.Data.Skip(4).Take(newBytes.Length).ToArray();
string msg = Encoding.UTF8.GetString(newBytes);
this.textBox_RevMsgList.Invoke(new MethodInvoker(() =>
{
this.textBox_RevMsgList.AppendText($"{DateTime.Now.ToString("MM-dd HH:mm:ss")} {msg}");
this.textBox_RevMsgList.AppendText(Environment.NewLine);
}));
}
}
void client_Closed(object sender, EventArgs e)
{
this.button_ConnectOrDisConnect.Invoke(new MethodInvoker(() =>
{
this.button_ConnectOrDisConnect.Text = @"连接";
}));
SetConnectStatus("已断开");
}
/// <summary>
/// 向服务器发命令行协议的数据
/// </summary>
/// <param name="key">命令名称</param>
/// <param name="data">数据</param>
public void SendCommand(string key, string data)
{
if (client.IsConnected)
{
byte[] arr = Encoding.Default.GetBytes(string.Format("{0} {1}", key, data));
client.Send(arr, 0, arr.Length);
}
else
{
throw new InvalidOperationException("未建立连接");
}
}
/// <summary>
/// 设置状态
/// </summary>
/// <param name="status"></param>
public void SetConnectStatus(string status)
{
this.label_ConnectStatus.Invoke(
new MethodInvoker(() =>
{
this.label_ConnectStatus.Text = $@"状态:{status}";
}));
}
private void Form_Main_Load(object sender, EventArgs e)
{
client = new AsyncTcpSession();
// 连接断开事件
client.Closed += client_Closed;
// 收到服务器数据事件
client.DataReceived += client_DataReceived;
// 连接到服务器事件
client.Connected += client_Connected;
// 发生错误的处理
client.Error += client_Error;
SetConnectStatus("未连接");
}
//关键
private void button_SendMsg_Click(object sender, EventArgs e)
{
if (client.IsConnected)
{
//消息体 这里编码要与tioServer的设置的编码一致
byte[] arr = Encoding.UTF8.GetBytes(textBox_SendMsg.Text);
//消息长度,占用字节长度要与tioServer的设置一致
byte[] arrayLengthHeader = ByteArrayUtil.IntToReverseBytes(arr.Length, 4);
//合成一个消息体
arr = ByteArrayUtil.PushNewByteArrayToByteArray(ref arrayLengthHeader, arr);
//发送消息
client.Send(arr, 0, arr.Length);
this.textBox_MsgSendList.AppendText(
$"{DateTime.Now.ToString("MM-dd HH:mm:ss")} {textBox_SendMsg.Text}");
this.textBox_MsgSendList.AppendText(Environment.NewLine);
}
else
{
throw new InvalidOperationException("未建立连接");
}
}
}
//下面是两个自己写的关键函数,可以放在FormMain中,也可以自己建立一个Util类
/**
* 将int数值转换为占1到4个字节的byte数组
* @param value
* 要转换的int值
* @return byte数组
*/
public static byte[] IntToReverseBytes(int value, int targetByteCount)
{
byte[] src = new byte[targetByteCount];
if (targetByteCount == 1)
{
src[0] = (byte)(value & 0xFF);
}
else if (targetByteCount == 2)
{
src[0] = (byte)((value >> 8) & 0xFF);
src[1] = (byte)(value & 0xFF);
}
else if (targetByteCount == 3)
{
src[0] = (byte)((value >> 16) & 0xFF);
src[1] = (byte)((value >> 8) & 0xFF);
src[2] = (byte)(value & 0xFF);
}
else if (targetByteCount == 4)
{
src[0] = (byte)((value >> 24) & 0xFF);
src[1] = (byte)((value >> 16) & 0xFF);
src[2] = (byte)((value >> 8) & 0xFF);
src[3] = (byte)(value & 0xFF);
}
return src;
}
/// <summary>
/// 压入新的Byte[]到Byte[]数组
/// </summary>
/// <param name="sourceBytes">源Byte[]</param>
/// <param name="newByte">新的Byte[]</param>
/// <returns>新的Byte[]</returns>
public static byte[] PushNewByteArrayToByteArray(ref byte[] sourceBytes, byte[] newByteArray)
{
Array.Resize<byte>(ref sourceBytes, sourceBytes.Length + newByteArray.Length);
for (int indexOfNewByteArray = 0, indexOfSourceBytes = sourceBytes.Length - newByteArray.Length;
indexOfNewByteArray < newByteArray.Length;
indexOfNewByteArray++, indexOfSourceBytes++)
{
sourceBytes[indexOfSourceBytes] = newByteArray[indexOfNewByteArray];
}
return sourceBytes;
}
通过试验,客户端和服务端可以接收消息并解析。
1、通过运行,发现服务端经常报错,错误如下:
2020-01-09 11:37:51,680 ERROR org.tio.server.ServerTioConfig[355]:
java.lang.NullPointerException: null
at org.tio.server.ServerTioConfig$1.run(ServerTioConfig.java:347)
at java.base/java.lang.Thread.run(Thread.java:834)
经过跟踪,是心跳未设置的原因(还未完全确定)
—-针对这个问题,刚刚阅读tio中ServerAioListener的代码后,我们只需要建立一个新类HelloServerAioListener(实现ServerAioListener接口),且在onHeartbeatTimeout方法中返回false即可(可能以后业务逻辑更加复杂后,会编写这个函数)。
代码如下:
package org.tio.study.helloworld.server;
import org.tio.core.ChannelContext;
import org.tio.core.intf.Packet;
import org.tio.server.intf.ServerAioListener;
public class HelloServerAioListener implements ServerAioListener {
@Override
public boolean onHeartbeatTimeout(ChannelContext channelContext, Long interval, int heartbeatTimeoutCount) {
return false;
}
@Override
public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {
}
@Override
public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize) throws Exception {
}
@Override
public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {
}
@Override
public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess) throws Exception {
}
@Override
public void onAfterHandled(ChannelContext channelContext, Packet packet, long cost) throws Exception {
}
@Override
public void onBeforeClose(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) throws Exception {
}
}
2、Winform中接收消息并未考虑粘包分包问题
鉴于对tio的不熟悉,这些缺陷留到后面的学习与实践中解决。
最新评论 我的评论
t-io为本站提供HTTP、WebSocket、Socket、页面渲染与压缩等服务,nginx为本站提供反向代理服务
© 2017-2023 钛特云 版权所有 | 浙ICP备17032976号 | 浙公网安备 33011802002129号