急急如律令!火速搭建一个C#即时通信系统!(附源码分享——高度可移植!)
(2016年3月更:由于后来了解到GGTalk开源即时通讯系统,因此直接采用了该资源用于项目开发,在此对作者表示由衷的感谢!)
——————————————————————————————————
人在外包公司,身不由己!各种杂七杂八的项目都要做,又没有自己的技术沉淀,每次涉足新的项目都倍感吃力,常常现学现卖,却不免处处碰壁!当然,话说回来,也是自己的水平有限在先,一马配一鞍,无奈也只能留在外包公司。
这不,就在上一周,领导下达一个任务:3天内搭建一个C#即时通讯系统,与原有的办公系统集成。
我正心里犯嘀咕;“网络编程自己就只知道一点皮毛啊,还是大学选修课上听老师讲的那一点东西,别说即时通讯了,以前也就只照着书上的例子写过一个抓包工具当作业交过,彻头彻尾的小白啊,何况都毕业几年了,连“套接字”都快忘了!”
领导补充说:“这个即时通讯系统要尽快完成,之后还有别的的项目。”
我:“······好的”
没办法,就像领导常说的“有条件要上,没有条件创造条件也要上!”,临危受命,唯有逆流而上!
想都别想,写即时通讯总不能从socket写起啊,那样写出来的东西只能读书的时候当作业交给老师看下,然后记一个平时成绩,给领导看那就是找抽!
所以,只能“登高而招,顺风而呼”,园子里大神多,资源也多,找找看有没有可以参考的。(这也是我一直以来的工作方法,呵呵)
终于,看到了一篇轻量级通信引擎StriveEngine通信demo源码研究学习了一下,稍加揣摩,很快就完成了领导所交付的重任!在此要鸣谢该文的作者!
言归正传,接下来就把自己的学习所得以及编写过程详尽的分享给大家!
一·C#即时通信系统界面快照
二·网络消息流与通信协议
首先,网络中的数据是源源不断的二进制流,有如长江之水连绵不绝,那么,即时通讯系统如何从连绵不绝的数据流中准确的识别出一个消息呢?换言之,在悠远绵长的网络数据流中,一个个具体的消息应该如何被界定出来呢?
这就需要用到通信协议。通信协议,一个大家耳熟能详的术语,什么TCP啊、UDP啊、IP啊、ICMP啊,以前学《计算机网络》时,各种协议充斥寰宇。但是,从教科书上抽象的概念中,你真的了解什么是通信协议吗?
回到开始的问题,我想恐怕可以这样来理解:通信协议就是要让消息遵循一定的格式,而这样的格式是参与通信的各方都知晓且遵守的,依据这样的格式,消息就能从数据流中被完整的识别出来。
通信协议的格式通常分为两类:文本消息、二进制消息。
文本协议相对简单,通常使用一个特殊的标记符作为一个消息的结束。这样一来,根据这个特殊的标志符,每个消息之间就有了明确的界限。
二进制协议,通常是由消息头(Header)和消息体(Body)构成的,消息头的长度固定,而且,通过解析消息头,可以知道消息体的长度。如此,我们便可以从网络流中解析出一个个完整的二进制消息。
两种协议各有优劣,虽然文本协议比较简单方便,但是二进制协议更具有普适性,诸如图片啊、文件啊都可以转化为二进制数组,所以我在写即时通讯时采用的是二进制协议。
我定义的二进制协议是:消息头固定为8个字节:前四个字节为一个int,其值表示消息类型;后四个字节也是一个int,其值表示消息体长度。
先来看消息头的定义
public class MsgHead
{
private int messageType;
/// <summary>
/// 消息类型
/// </summary>
public int MessageType
{
get { return messageType; }
set { messageType = value; }
} private int bodyLength;
/// <summary>
/// 消息体长度
/// </summary>
public int BodyLength
{
get { return bodyLength; }
set { bodyLength = value; }
} public const int HeadLength = ; public MsgHead(int msgType,int bodyLen)
{
this.bodyLength = bodyLen;
this.messageType = msgType;
} public byte[] ToStream()
{
byte[] buff = new byte[MsgHead.HeadLength];
byte[] bodyLenBuff = BitConverter.GetBytes(this.bodyLength);
byte[] msgTypeBuff = BitConverter.GetBytes(this.messageType);
Buffer.BlockCopy(msgTypeBuff, , buff, , msgTypeBuff.Length);
Buffer.BlockCopy(bodyLenBuff, , buff, , bodyLenBuff.Length);
return buff;
}
}
然后我们将识别消息的方法封装到一个协议助手类中,即收到消息的时候,明确如下两个问题:1.固定前多少位是消息头。2.如何从消息头中获取消息体长度。
public class StreamContractHelper : IStreamContractHelper
{
/// <summary>
/// 消息头长度
/// </summary>
public int MessageHeaderLength
{
get { return MsgHead.HeadLength; }
}
/// <summary>
/// 从消息头中解析出消息体长度,从而可以间接取出消息体
/// </summary>
/// <param name="head"></param>
/// <returns></returns>
public int ParseMessageBodyLength(byte[] head)
{
return BitConverter.ToInt32(head,);
}
}
三·通信协议类
然后我们来定义满足协议的消息基类,其中重点是要定义ToContractStream()方法,使得消息能够序列化成满足协议的二进制流,从而通过网络进行传输。
[Serializable]
public class BaseMsg
{
private int msgType; public int MsgType
{
get { return msgType; }
set { msgType = value; }
}
/// <summary>
/// 序列化为本次通信协议所规范的二进制消息流
/// </summary>
/// <returns></returns>
public byte[] ToContractStream()
{
return MsgHelper.BuildMsg(this.msgType, SerializeHelper.SerializeObject(this));
}
}
然后我们来看看MsgHelper类的具体实现
public static class MsgHelper
{
/// <summary>
/// 构建消息
/// </summary>
/// <param name="msgType">消息类型</param>
/// <param name="msgBody">消息体</param>
/// <returns></returns>
public static byte[] BuildMsg(int msgType, Byte[] msgBody)
{
MsgHead msgHead = new MsgHead(msgType, msgBody.Length);
//将消息头与消息体拼接起来
byte[] msg = BufferJointer.Joint(msgHead.ToStream(), msgBody);
return msg;
} public static byte[] BuildMsg(int msgType, string str)
{
return MsgHelper.BuildMsg(msgType, Encoding.UTF8.GetBytes(str));
}
/// <summary>
/// 将二进制数组还原成消息对象
/// </summary>
/// <typeparam name="T">所要还原成的消息类</typeparam>
/// <param name="msg">消息数据</param>
/// <returns></returns>
public static T DeserializeMsg<T>(byte[] msg)
{
return (T)SerializeHelper.DeserializeBytes(msg, , msg.Length - );
}
}
然后我们再看一个具体的消息类ChatMsg的定义
[Serializable]
public class ChatMsg:BaseMsg
{
private string sourceUserID;
/// <summary>
/// 发送该消息的用户ID
/// </summary>
public string SourceUserID
{
get { return sourceUserID; }
set { sourceUserID = value; }
}
private string targetUserID;
/// <summary>
/// 该消息所发往的用户ID
/// </summary>
public string TargetUserID
{
get { return targetUserID; }
set { targetUserID = value; }
}
private DateTime timeSent;
/// <summary>
/// 该消息的发送时间
/// </summary>
public DateTime TimeSent
{
get { return timeSent; }
set { timeSent = value; }
}
private string msgText;
/// <summary>
/// 该消息的文本内容 ///
/// </summary>
public string MsgText
{
get { return msgText; }
set { msgText = value; }
}
/// <summary>
/// 构造一个ChatMsg实例
/// </summary>
/// <param name="_sourceUserID">该消息源用户ID</param>
/// <param name="_targetUserID">该消息目标用户ID</param>
/// <param name="_MsgText">该消息的文本内容 </param>
public ChatMsg(string _sourceUserID, string _targetUserID, string _MsgText)
{
base.MsgType = Core.MsgType.Chatting;
this.sourceUserID = _sourceUserID;
this.targetUserID = _targetUserID;
this.timeSent = DateTime.Now;
this.msgText = _MsgText;
}
}
四·登录的通信过程
1.客户端发送登陆消息
private void button_login_Click(object sender, EventArgs e)
{
this.selfID = this.textBox_ID.Text.Trim();
LoginMsg loginMsg = new LoginMsg(this.selfID);
this.tcpPassiveEngine.PostMessageToServer(loginMsg.ToContractStream());
}
2.服务端回复登陆消息
if (msgType == MsgType.Logining)
{
LoginMsg loginMsg = MsgHelper.DeserializeMsg<LoginMsg>(msg);
this.ReplyLogining(loginMsg, userAddress);
//将在线用户告知其他客户端
this.TellOtherUser(MsgType.NewOnlineFriend, loginMsg.SrcUserID);
} /// <summary>
/// 回复登陆消息
/// </summary>
/// <param name="loginMsg"></param>
/// <param name="userAddress"></param>
private void ReplyLogining(LoginMsg loginMsg, IPEndPoint userAddress)
{
if (this.onlineManager.Contains(loginMsg.SrcUserID))//重复登录
{
loginMsg.LogonResult = LogonResult.Repetition;
this.tcpServerEngine.SendMessageToClient(userAddress, loginMsg.ToContractStream());
}
else//此demo简化处理回复成功,其他验证未处理
{
this.AddUser(loginMsg.SrcUserID, userAddress);
this.ShowOnlineUserCount();
loginMsg.LogonResult = LogonResult.Succeed;
this.tcpServerEngine.SendMessageToClient(userAddress, loginMsg.ToContractStream());
}
}
3.客户端处理登陆结果
private void tcpPassiveEngine_MessageReceived(IPEndPoint userAddress, byte[] msg)
{
//取出消息类型
int msgType = BitConverter.ToInt32(msg, );
//验证消息类型
if (msgType == MsgType.Logining)
{
LoginMsg loginMsg = MsgHelper.DeserializeMsg<LoginMsg>(msg);
if (loginMsg.LogonResult == LogonResult.Succeed)
{
this.DialogResult = DialogResult.OK;
this.tcpPassiveEngine.MessageReceived -= new StriveEngine.CbDelegate<IPEndPoint, byte[]>(tcpPassiveEngine_MessageReceived);
}
if (loginMsg.LogonResult == LogonResult.Repetition)
{
MessageBox.Show("登录失败,该账号已经登录!");
}
}
}
五·即时通信客户端通信过程
1.客户端A发送聊天消息给服务器
/// <summary>
/// 发送聊天消息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button_send_Click(object sender, EventArgs e)
{
string chatText = this.richTextBox_Write.Text;
if (string.IsNullOrEmpty(chatText))
{
MessageBox.Show("消息不能为空");
return;
}
ChatMsg chatMsg = new ChatMsg(this.selfUserID, this.friendID, chatText);
this.tcpPassiveEngine.SendMessageToServer(chatMsg.ToContractStream());
this.ShowChatMsg(chatMsg);
}
2.服务端转发聊天消息
if (msgType == MsgType.Chatting)
{
ChatMsg chatMsg = MsgHelper.DeserializeMsg<ChatMsg>(msg);
if (this.onlineManager.GetKeyList().Contains(chatMsg.TargetUserID))
{
IPEndPoint targetUserAddress = this.onlineManager.Get(chatMsg.TargetUserID).Address;
this.tcpServerEngine.SendMessageToClient(targetUserAddress, msg);
}
}
3.客户端B接收并显示聊天消息
void tcpPassiveEngine_MessageReceived(IPEndPoint userAddress, byte[] msg)
{
//取出消息类型
int msgType = BitConverter.ToInt32(msg, );
//验证消息类型
if (msgType == MsgType.Chatting)
{
ChatMsg chatMsg = MsgHelper.DeserializeMsg<ChatMsg>(msg);
this.ShowChatForm(chatMsg.SourceUserID);
this.ChatMsgReceived(chatMsg);
}
} /// <summary>
/// 显示聊天窗
/// </summary>
/// <param name="friendUserID">聊天对方用户ID</param>
private void ShowChatForm(string friendUserID)
{
if (this.InvokeRequired)
{
this.Invoke(new CbGeneric<string>(this.ShowChatForm), friendUserID);
}
else
{
ChatForm form = this.chatFormManager.GetForm(friendUserID);
if (form == null)
{
form = new ChatForm(this.selfID, friendUserID, this, this.tcpPassiveEngine);
form.Text = string.Format("与{0}对话中···", friendUserID);
this.chatFormManager.Add(form);
form.Show();
}
form.Focus();
}
} /// <summary>
/// 显示聊天消息
/// </summary>
/// <param name="chatMsg"></param>
private void ShowChatMsg(ChatMsg chatMsg)
{
if (this.InvokeRequired)
{
this.Invoke(new CbGeneric<ChatMsg>(this.formMain_chatMsgReceived), chatMsg);
}
else
{
this.richTextBox_display.AppendText(chatMsg.SourceUserID + " " + chatMsg.TimeSent.ToString() + "\r\n");
this.richTextBox_display.AppendText(chatMsg.MsgText + "\r\n");
this.richTextBox_Write.Clear();
}
}
五·C#即时通信源码下载
源码说明:1.客户端与服务端均含有配置文件,可配置进程的IP与端口号。
2.代码均含有详细注释。
3.调试时确保客户端的配置文件相关信息无误,先启动服务端再启动客户端。
4.登录账号与密码均为任意。
5.点击好友头像即可聊天。
下载:Chat.Demo
附相关系列源码:
版权声明:本文为博主原创文章,未经博主允许不得转载。
急急如律令!火速搭建一个C#即时通信系统!(附源码分享——高度可移植!)的更多相关文章
- 【MVVMLight小记】二.开发一个简单图表生成程序附源码
上一篇文章介绍了怎样快速搭建一个基于MVVMLight的程序http://www.cnblogs.com/whosedream/p/mvvmlight1.html算是简单入门了下,今天我们来做一个稍许 ...
- vue新手入门之使用vue框架搭建用户登录注册案例,手动搭建webpack+Vue项目(附源码,图文详解,亲测有效)
前言 本篇随笔主要写了手动搭建一个webpack+Vue项目,掌握相关loader的安装与使用,包括css-loader.style-loader.vue-loader.url-loader.sass ...
- 【原创】使用JS封装的一个小型游戏引擎及源码分享
1 /** * @description: 引擎的设计与实现 * @user: xiugang * @time: 2018/10/01 */ /* * V1.0: 引擎实现的基本模块思路 * 1.创建 ...
- 使用Xamarin开发即时通信系统 -- 基础篇(大量图文讲解 step by step,附源码下载)...
如果是.NET开发人员,想学习手机应用开发(Android和iOS),Xamarin 无疑是最好的选择,编写一次,即可发布到Android和iOS平台,真是利器中的利器啊!而且,Xamarin已经被微 ...
- Ext.NET 4.1 系统框架的搭建(后台) 附源码
Ext.NET 4.1 系统框架的搭建(后台) 附源码 代码运行环境:.net 4.5 VS2013 (代码可直接编译运行) 预览图: 分析图: 上面系统的构建包括三块区域:North.West和C ...
- 开源方案搭建可离线的精美矢量切片地图服务-8.mapbox 之sprite大图图标文件生成(附源码)
项目成果展示(所有项目文件都在阿里云的共享云虚拟主机上,访问地图可以会有点慢,请多多包涵). 01:中国地图:http://test.sharegis.cn/mapbox/html/3china.ht ...
- 轻量级通信引擎StriveEngine —— C/S通信demo(附源码)
前段时间,有几个研究ESFramework的朋友对我说,ESFramework有点庞大,对于他们目前的项目来说有点“杀鸡用牛刀”的意思,因为他们的项目不需要文件传送.不需要P2P.不存在好友关系.也不 ...
- MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)
前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...
- 轻量级通信引擎StriveEngine —— C/S通信demo(2) —— 使用二进制协议 (附源码)
在网络上,交互的双方基于TCP或UDP进行通信,通信协议的格式通常分为两类:文本消息.二进制消息. 文本协议相对简单,通常使用一个特殊的标记符作为一个消息的结束. 二进制协议,通常是由消息头(Head ...
随机推荐
- docker简单介绍----Dockerfile命令
DockerFile的组成部署: 下面优先介绍下Dcokerfile的基础指令 一.CMD指令:容器启动时要莫热门运行的命令,如果有多个CMD指定,最后一个生效 使用方法: CMD ["ex ...
- python之地基(二)
上一个阶段呢,我们已经学习了python的数据的类型.今天呢,我们来学习各种各样的运算符. 一.基本运算符 a = 10 b = 20 运算符号 描述 示例 + 加——两个对象相加 a+b 输出 ...
- webpack 学习小结
webpack 是一个模块打包工具(前提要安装 node使用npm来安装webpack) 1.安装webpack,webpack-cli , webpack-dev-server //全局安装 npm ...
- PADS Layout VX.2.3 更改Logic符号,并更新到原理图
操作系统:Windows 10 x64 工具1:PADS Layout VX.2.3 统一使用A3尺寸的原理图,如果Logic符号画得太大,就会占用过多的面积.如下图所示,电阻的两只引脚的跨度占用了4 ...
- python数据类型之基础进阶
一: 解构 1.1 结构字符串 变量和字符个数必须严格一致 name = 'wc' a,b=name print(a) print(b) # w # c name = 'w' a,b=name pri ...
- Zabbix (三)
一.zabbix支持的主要监控方式: zabbix主要Agent,Trapper,SNMP,JMX,IPMI这几种监控方式,本文章主要通过监控理论和实际操作测试等方式来简单介绍这几种方式的监控原理和优 ...
- C++函数返回值为类对象但未调用复制构造函数
参考资料:https://blog.csdn.net/sxhelijian/article/details/50977946 不要迷信书本,要学会自己调试程序.
- C++ 初读迭代器
迭代器 这是个啥? string对象或vector对象可以通过下标访问每一个元素,迭代器也具有同样的效果.那又有什么不同呢?事实上并不是所有的容器到可以使用下标访问每一个元素,即在容器上迭代器更具普适 ...
- python中for嵌套打印图形
# 打印出九九乘法表 1 * 1 = 1 2 * 1 = 2 2 * 2 = 4 3 * 1 = 3 3 * 2 = 6 3 * 3 = 9 4 * 1 = 4 4 * 2 = 8 4 * 3 = 1 ...
- look back to 2018
只写展望怎么行,还是缺一篇总结.2018年几乎没有怎么发朋友圈,需要一些文字记录一下这一年发生的事. 去年的现在,2018年的开端,结束了研一上学期充实的生活,下学期一项艰巨的任务就是完成大项目,一个 ...