最后在公司實習,新人不給活干,就自己隨便看看,了解一些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傳輸文件 這塊
這篇文章大致就這樣啦,如果大家有什么疑問或者建議趕快留言吧,還有本源碼和軟件僅供交流學習,用於商業或者實際應用了出了問題概不負責哈