用c#写的一个局域网聊天客户端 类似小飞鸽
最后在公司实习,新人不给活干,就自己随便看看,了解一些DevExpress控件啊,编码规范啊之类的,自己就寻思着写一点点小东西练习练习
出于自己对c# socket这块不熟,就选择了这块,顺便可以进一步了解委托 代理。
闲话不说,先说下这次做的东西:一个局域网聊天的小软件 主要基于udp的通信,如果读者还不知道udp or tcp 那请度娘一下。。。
基本思路(这也都是网上查的,还查了飞鸽传书的 基本原理,在此感谢网上的各位高手哈):
1:软件开启的时候先新开一个线程,该线程充当服务器端,一直死循环监听
2:开了新线程了,调用广播的方法
3:此时如果局域网内已经有有其它主机打开了这个软件,将会监听到这个广播,收到这个广播后将返回自己的主机,并且将监听到的主机添加自己的在线列表中,当然,发起广播的软件也能收到其它软件的回信,收到他们的主机后也加入自己的在线列表,这样各自的列表就都能建立起来,并且将当前的列表加入一个静态的泛型列表中(用于以后和其它用户的通信,维护他们的状态)
4:通信 发送消息:双击一个主机列表后 得到该主机host 传到交谈窗体 并查询出他主机的endpoint,这样就可以进行本机和向该endpoint点发送消息了
5:下线 下线之前软件会发一个下线的广播,其它的软件接到该广播的时候将会将该主机从自己的在线列表中移除
整体思路就这样,下面可以结合代码具体看一下
#region Field /// <summary> /// 在非主线程是对控件操作 /// </summary> /// <param name="host"></param> private delegate void MyInvoke(string host); /// <summary> /// 充当服务器 进行监听接收 /// </summary> private SocketUdpServer udpServer; /// <summary> /// 充当客户端 /// </summary> private SocketUdpClient udpClient; #endregion #region Contructor /// <summary> /// 构造函数 /// </summary> public FrmUser() { InitializeComponent(); init(); } #endregion #region Method /// <summary> /// 初始化数据 /// </summary> private void init() { LanList.CurrentLanList = new List<LanInfo>(); this.Text = "当前局域网内在线用户"; this.udpServer = SocketUdpServer.Instance; this.udpServer.OnLineComplete += new SocketUdpServer.OnCompleteHander(this.OnLine_Event); this.udpServer.DownLineComplete += new SocketUdpServer.OnCompleteHander(this.DownLine_Event); this.udpServer.ErrorAppear += new SocketUdpServer.OnCompleteHander(this.Error_Event); this.udpServer.Listen(); this.udpClient = new SocketUdpClient(); this.udpClient.Broadcast(DatagramType.OnLine); } /// <summary> /// 上线增加用户 /// </summary> /// <param name="host">用户主机</param> private void AddUser(string host) { this.ilbUserList.Items.Add(host, 0); } /// <summary> /// 下线减少用户 /// </summary> /// <param name="host">用户主机在列表的序号 懒了以下 应该将回调的委托参数定义为int的,这里用了string 到程序需要转化为Int</param> private void RemoveUser(string hostIndex) { this.ilbUserList.Items.RemoveAt(Convert.ToInt32(hostIndex)); } #endregion #region Event /// <summary> /// 上线事件 /// </summary> /// <param name="socket"></param> /// <param name="e"></param> private void OnLine_Event(SocketUdpServer socket, EventArgs e) { string host = socket.Message; //如果该上线的用户在局域网列表中不存在 if (!LanList.CurrentLanList.Exists(x => x.Host == host)) { while (!this.IsHandleCreated) ; this.ilbUserList.Invoke(new MyInvoke(this.AddUser), host); //将上线用户添加进静态的局域网列表 LanList.CurrentLanList.Add(new LanInfo() { Host = host, State = TalkState.Waiting, RemoteEndPoint = socket.RemoteEndPoint }); } } /// <summary> /// 下线事件 /// </summary> /// <param name="socket"></param> /// <param name="e"></param> private void DownLine_Event(SocketUdpServer socket, EventArgs e) { string host = socket.Message; if (LanList.CurrentLanList.Exists(x => x.Host == host)) { ///判断是否是自己的主机下线 如果是自己的 则不需要操作 if (string.Compare(Dns.GetHostName(), host) != 0) { this.ilbUserList.Invoke(new MyInvoke(this.RemoveUser), LanList.CurrentLanList.FindIndex(x => x.Host == host).ToString()); //将该用户从局域网列表中移除 LanList.CurrentLanList.RemoveAll(x => x.Host == host); } } } /// <summary> /// 出现错误的事件 /// </summary> /// <param name="socket"></param> /// <param name="e"></param> private void Error_Event(SocketUdpServer socket, EventArgs e) { XtraMessageBox.Show(socket.Message); } private void ilbUserList_DoubleClick(object sender, EventArgs e) { //XtraMessageBox.Show(ilbUserList.SelectedItem.ToString()); string host = ilbUserList.SelectedItem.ToString(); ///打开窗口 设置为正在交谈 LanList.SetTalkState(host, TalkState.Talking); (new FrmTalk(host)).Show(); } /// <summary> /// 窗体关闭事 进行下线广播 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void FrmUser_FormClosed(object sender, FormClosedEventArgs e) { this.udpClient.Broadcast(DatagramType.DownLine); this.udpServer.Stop(); Application.Exit(); } /// <summary> /// 刷新按钮 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnRefresh_Click(object sender, EventArgs e) { //刷新 情况列表中的数据 重新上线广播 this.ilbUserList.Items.Clear(); LanList.CurrentLanList.Clear(); this.udpClient.Broadcast(DatagramType.OnLine); } #endregion
该页面主要是在线用户列表页面,同时监听其它软件发来的上线,下线,获取主机信息等数据报,维护当前在线的用户 和聊天状态
#region Field /// <summary> /// 自己的socket 充当服务器 接收消息 /// </summary> private SocketUdpServer selfSocket = null; /// <summary> /// 对话的socket /// </summary> private SocketUdpClient tallSocket = null; /// <summary> /// 谈话对方的局域网信息 /// </summary> private LanInfo talkLan = null; /// <summary> /// 当前用户主机 /// </summary> private string currentUserHost = ""; /// <summary> /// 对控件操作 在非主线程下需要调用此代理 /// </summary> private delegate void MyInvoke(string user,string message); #endregion #region Constructor /// <summary> /// 通过远端主机名打开窗体 /// </summary> /// <param name="host"></param> public FrmTalk(string host) { InitializeComponent(); if (this.talkLan == null) { this.talkLan = LanList.CurrentLanList.Find(x => x.Host == host); } this.currentUserHost = Dns.GetHostName(); this.Text = "正在和" + host + "聊天中"; this.Initializion(); } /// <summary> /// 通过远端 端点打开窗体 /// </summary> /// <param name="remotePoint"></param> public FrmTalk(EndPoint remotePoint) { this.talkLan = LanList.CurrentLanList.Find(x => string.Compare(x.RemoteEndPoint.ToString(), remotePoint.ToString()) == 0); (new FrmTalk(talkLan.Host)).Show(); } #endregion #region Method /// <summary> /// 初始化方法 /// </summary> private void Initializion() { this.selfSocket = SocketUdpServer.Instance; ///绑定收到信息事件 this.selfSocket.OnChatComplete += new SocketUdpServer.OnCompleteHander(this.ReceiveEvent); //给谈话的socket初始化endpoint this.tallSocket = new SocketUdpClient(this.talkLan.RemoteEndPoint); } /// <summary> /// 加载未读的信息 /// </summary> private void LoadUnReadMessage() { Queue<MessageInfo> queque = QueueMessage.GetAndRemove(talkLan.Host); MessageInfo messageInfo=null; if (queque != null) { while (queque.Count > 0) { //出队列 messageInfo = queque.Dequeue(); this.lbxMessage.Items.Add(talkLan.Host + ":" + messageInfo.ReceiveTime.ToString("yyyy-MM-dd HH:mm:ss")); this.lbxMessage.Items.Add(messageInfo.Message); } } } /// <summary> /// 添加一行 在listboxcontrol中 /// </summary> /// <param name="name">显示的用户</param> /// <param name="message">消息</param> private void AddLine(string name,string message) { this.lbxMessage.Items.Add(name+ ":" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); this.lbxMessage.Items.Add(message); } /// <summary> /// 发送信息 由键盘回车和发送按钮调用 /// </summary> private void SendMessage() { try { string message = this.memInput.Text; if (string.IsNullOrEmpty(message)) { XtraMessageBox.Show("发送信息不能为空"); } else { this.tallSocket.Send(message); this.AddLine("我", message); this.memInput.Text = ""; } } catch (Exception ex) { XtraMessageBox.Show(ex.Message); } } #endregion #region Event /// <summary> /// 表单加载 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void FrmTalk_Load(object sender, EventArgs e) { this.LoadUnReadMessage(); } /// <summary> /// 接收信息回调事件 /// </summary> /// <param name="socket"></param> /// <param name="e"></param> private void ReceiveEvent(SocketUdpServer socket, EventArgs e) { //判断 远端的网络端点是否是当前的 打开的窗体 if (string.Compare(this.talkLan.RemoteEndPoint.ToString(), socket.RemoteEndPoint.ToString()) == 0) { this.lbxMessage.Invoke(new MyInvoke(this.AddLine), this.talkLan.Host, socket.Message); } } /// <summary> /// 信息发送按钮 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSend_Click(object sender, EventArgs e) { this.SendMessage(); } private void FrmTalk_FormClosed(object sender, FormClosedEventArgs e) { //将其设置为非交谈状态 LanList.SetTalkState(talkLan.Host, TalkState.Waiting); } private void memInput_KeyDown(object sender, KeyEventArgs e) { ///按下回车事件 if (e.KeyCode == Keys.Enter) { this.SendMessage(); } } #endregion
该页面就是聊天页面,主要是对相应的host进行通信聊天,发送和接收聊天信息,根据聊天窗口设置状态啊之类的
#region Method #region 停止当前监听和断开线程 /// <summary> /// 停止当前服务器的监听和断开线程 /// </summary> public void Stop() { this.listenThread.Abort(); this.listenSocket.Close(); } #endregion #region 监听 /// <summary> /// 开始监听 /// </summary> public void Listen() { ThreadStart method = new ThreadStart(this.ListenMethod); this.listenThread = new Thread(method); this.listenThread.Start(); } /// <summary> /// 监听的方法 /// </summary> private void ListenMethod() { try { this.listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint ipep = new IPEndPoint(IPAddress.Any, this.port); this.listenSocket.Bind(ipep);//定义一个网络端点 IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);//定义要发送的计算机的地址 EndPoint remote = (EndPoint)(sender);//远程 ///持续监听 while (true) { byte[] data = new byte[1024]; //准备接收 int recv = this.listenSocket.ReceiveFrom(data, ref remote); string stringData = Encoding.UTF8.GetString(data, 0, recv); //将接收到的信息转化为自定义的数据报类 Datagram recvicedataGram = Datagram.Convert(stringData); this.message = recvicedataGram.Message; string remotePoint = remote.ToString(); string remoteip = remotePoint.Substring(0, remotePoint.IndexOf(":")); remote = new IPEndPoint(IPAddress.Parse(remoteip), this.port); this.remoteEndPoint = remote; this.Action(recvicedataGram.Type); } } catch (Exception ex) { this.message = ex.Message; this.ErrorAppear(this, new EventArgs()); } } /// <summary> /// 收到数据报后的动作 /// </summary> /// <param name="type">数据报的类型</param> private void Action(DatagramType type) { switch (type) { case DatagramType.OnLine: Datagram sendDataGram = new Datagram { Type = DatagramType.GiveInfo, FromAddress = "", ToAddress = "", Message = Dns.GetHostName() }; //告诉对方自己的信息 this.listenSocket.SendTo(Encoding.UTF8.GetBytes(sendDataGram.ToString()), this.remoteEndPoint); this.OnLineComplete(this, new EventArgs()); break; case DatagramType.GiveInfo: ///执行添加上线用户事件 this.OnLineComplete(this, new EventArgs()); break; case DatagramType.DownLine: ///执行用户下线事件 ///如果是自己下线 if (string.Compare(Dns.GetHostName(), message) == 0) { System.Windows.Forms.Application.Exit(); } else { this.DownLineComplete(this, new EventArgs()); } break; case DatagramType.Chat: //得到当前要交谈的用户 LanInfo lanInfo = LanList.CurrentLanList.Find(x => string.Compare(this.remoteEndPoint.ToString(), x.RemoteEndPoint.ToString()) == 0); //如果有查询到该用户在自己这边登记过 if (lanInfo != null) { if (lanInfo.State == TalkState.Talking) { //正在交谈 直接打开这次窗口 this.OnChatComplete(this, new EventArgs()); } else { //没有交谈 将窗口加入信息的队列 MessageInfo messageInfo = new MessageInfo() { Message = this.message, ReceiveTime = DateTime.Now, RemoteEndPoint = this.remoteEndPoint }; QueueMessage.Add(lanInfo.Host, messageInfo); } } break; } } #endregion #endregion
充当服务器的 socket的监听,定义一些监听事件,在form里面使用该事件就可以了
#region Delegate Event /// <summary> /// 完成一个socket的代理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public delegate void OnCompleteHander(SocketUdpServer sender, EventArgs e); /// <summary> /// 完成收到一个主机信息 即上线事件 /// </summary> public event OnCompleteHander OnLineComplete; /// <summary> /// 完成下线事件 /// </summary> public event OnCompleteHander DownLineComplete; /// <summary> /// 完成一次谈话 就一条信息 /// </summary> public event OnCompleteHander OnChatComplete; /// <summary> /// 有错误出现 /// </summary> public event OnCompleteHander ErrorAppear; #endregion
用户上线事件,下线事件,或者主机事件,chat聊天事件,再服务器接收到信息后 感觉信息分类执行不同的事件
在CHAT类型的数据报重要注意的是,当有数据过来接收到 但是该主机窗口并未打开时,要将要收到信息加入一个未读的信息队列中,当再次开发对该用户的聊天窗口时先要加载相应的未读信息队列,这样可以简单的实现离线信息的发送
接下来看下信息数据报的格式
/***************************************************************** * 定义广播的数据格式 * Type=OnLine,FromAdress=xxx,ToAdress=zzz,Message=mmm * 类型为上线广播 从xxx主机到zzz主机 信息是mmm * CHAT这个就是我的信息我的信息 可能有各种=,的字符串 * 这种就直接将CHAT去掉后 后面的都为mmm *****************************************************************/ /// <summary> /// 定义数据报里面的几个字段 /// </summary> public class Datagram { #region Property /// <summary> /// 数据报的类型 , /// </summary> public DatagramType Type { get; set; } /// <summary> /// 发送者的网络地址 /// </summary> public string FromAddress { get; set; } /// <summary> /// 接收者网络地址 /// </summary> public string ToAddress { get; set; } /// <summary> /// 数据报的信息 /// </summary> public string Message { get; set; } /// <summary> /// 信息 Message的长度 /// </summary> public int Length { get { return this.Message.Length; } } #endregion #region Method /// <summary> /// 重写下ToString /// </summary> /// <returns></returns> public override string ToString() { StringBuilder sb = new StringBuilder(); sb.AppendFormat("Type={0},", this.Type.ToString()); sb.AppendFormat("FromAddress={0},", this.FromAddress.ToString()); sb.AppendFormat("ToAddress={0},", this.ToAddress.ToString()); sb.AppendFormat("Message={0}", this.Message.ToString()); return sb.ToString(); } /// <summary> /// 将有效字符串转化成数据报 /// </summary> /// <param name="str"></param> /// <returns></returns> public static Datagram Convert(string str) { Datagram data = new Datagram(); //前面不是CHAT主要是建立连接 取消连接等信号传送 if (!str.StartsWith("CHAT")) { IDictionary<string, string> idict = new Dictionary<string, string>(); string[] strlist = str.Split(','); for (int i = 0; i < strlist.Length; i++) { //数据报字符串的各个键值对放进字典类 string[] info = strlist[i].Split('='); idict.Add(info[0], info[1]); } data.Type = (DatagramType)Enum.Parse(typeof(DatagramType), idict["Type"]); data.FromAddress = idict["FromAddress"]; data.ToAddress=idict["ToAddress"]; data.Message = idict["Message"]; } else { data.Type = (DatagramType)Enum.Parse(typeof(DatagramType), "Chat"); data.Message = str.Substring(4); } return data; } #endregion } #region Enum /// <summary> /// 数据报的类型 /// </summary> public enum DatagramType { /// <summary> /// 上线 一应一答 /// </summary> OnLine=1, /// <summary> /// 下线 一应 /// </summary> DownLine, /// <summary> /// 确认收到 一应 /// </summary> /// <summary> /// 正常聊天 一应一答 /// </summary> Chat, /// <summary> /// 给予个人的信息 /// </summary> GiveInfo } #endregion
简单的定义一下发送的数据报的格式 可能发送的几种类型:
上线:主要用于软件刚刚开启时向局域网内发送上线广播
下线:软件在关闭之前再向局域网内发送一次下线广播
给出主机信息:用于收到上线广播后 再返回一个自己主机信息给对方,让让对方知道局域网中这台主机是上线的
聊天:就是平常的通信 这里特别注意的是,为考虑到聊天中也会出来,= 这两个协定的字符串,所以 开头加CHAT 表示纯粹聊天的数据报
/// <summary> /// udp的客户端 主要用户发送数据 /// </summary> public class SocketUdpClient { #region Feild /// <summary> /// 广播的socket /// </summary> private Socket broadcastSocket; /// <summary> /// 服务器的端口 /// </summary> private int port; /// <summary> /// 远端的端点 /// </summary> private EndPoint remoteEndPoint = null; /// <summary> /// 当前客户端 /// </summary> private Socket client = null; #endregion #region Constructor /// <summary> /// 构造函数 /// </summary> public SocketUdpClient(EndPoint point) { this.client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); this.remoteEndPoint = point; } /// <summary> /// 无参构造函数 /// </summary> public SocketUdpClient() { this.port = 9050; } #endregion #region 进行广播 /// <summary> /// 进行广播 上线或者下线 /// </summary> /// <param name="msg">广播中发送的信息</param> public void Broadcast(DatagramType type) { this.broadcastSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, this.port); this.broadcastSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); Datagram dataGram = new Datagram { Type = type, FromAddress = "", ToAddress = "", Message = Dns.GetHostName() }; //将要发送的信息改为字节流 byte[] data = Encoding.ASCII.GetBytes(dataGram.ToString()); this.broadcastSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); this.broadcastSocket.SendTo(data, iep); //this.broadcastSocket.Close(); } #endregion #region Method /// <summary> /// 发送数据 /// </summary> /// <param name="message">当前的数据</param> public void Send(string message) { byte[] data = Encoding.UTF8.GetBytes("CHAT" + message); int i = client.SendTo(data, this.remoteEndPoint); } #endregion }
socket的client代码 实现广播 发送信息
以上简单的逻辑设计+代码就基本完成了这个简单的客户端聊天软件
说了那么多,接下来看下效果图:
本机这边的效果
局域网中另一端的效果
可以实现简单的 通讯
下面是源码下载:猛击我去下载它
大家在下载包中可以发现 有两个项目 一个是ITalk,他是我最初在写的时候使用的,窗体时继承dev的,效果稍微好一点
为考虑到各大读者可能没有安装dev,所以又一模一样的改了一个ITalkTradition,传统的winform
还有源码里面有可能socket的tcp 这块,这个主要是因为我刚刚开始的想法是使用tcp,但是后来发现请求在线用户,聊天之类都比较麻烦,就改用了udp,不过 本人有精力的话 还是要去试着写写tcp传输文件 这块
这篇文章大致就这样啦,如果大家有什么疑问或者建议赶快留言吧,还有本源码和软件仅供交流学习,用于商业或者实际应用了出了问题概不负责哈