C#局域網(wǎng)聊天工具、消息推送實(shí)現(xiàn)思路與源碼
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
C#局域網(wǎng)聊天工具怎么實(shí)現(xiàn)? 1. 網(wǎng)絡(luò)通訊編程的基礎(chǔ)便是協(xié)議,信息的發(fā)送常用的協(xié)議有面向連接的TCP協(xié)議,以及不面向連接的UDP協(xié)議 2. TCP:TransmissionControlProtocol傳輸控制協(xié)議,其是一種面向連接的、可靠的字節(jié)流服 務(wù)。面向連接意味著兩個(gè)使用TCP的應(yīng)用(通常是一個(gè)客戶和一個(gè)服務(wù)器)在彼此交換數(shù)據(jù)之前必須先建立一個(gè)TCP連接。這一過程與打電話很相似,先撥號(hào)振鈴,等待對(duì)方摘機(jī)說“喂”,然后才說明是誰。 3. UDP:UserDatagramProtocol用戶數(shù)據(jù)報(bào)協(xié)議(RFC768),UDP傳送數(shù)據(jù)前并不與對(duì)方建立連 接,即UDP是無連接的,在傳輸數(shù)據(jù)前,發(fā)送方和接收方相互交換信息使雙方同步。 4. 系統(tǒng)也要定義自己的通訊協(xié)議,來完成一些系統(tǒng)的功能,如用戶上,下線的通知,都要定義 自己的通訊協(xié)議來完成相應(yīng)的功能!也可以稱這種自定義的協(xié)議為“命令”. 5. 下面以著名的飛鴿傳書為例,說明其自定義的協(xié)議(命令) IPMSG_NOOPERATION不進(jìn)行任何操作 IPMSG_BR_ENTRY用戶上線 IPMSG_BR_EXIT用戶退出 IPMSG_ANSENTRY通報(bào)在線 IPMSG_SENDMSG發(fā)送消息 IPMSG_RECVMSG通報(bào)收到消息 IPMSG_GETFILEDATA請(qǐng)求通過TCP傳輸文件 IPMSG_RELEASEFILES停止接收文件 IPMSG_GETDIRFILES請(qǐng)求傳輸文件夾以“IPMSG_BR_ENTRY用戶上線”和“IPMSG_ANSENTRY通報(bào)在線”為例說明命令處理流程:當(dāng)程序啟動(dòng)時(shí),命令I(lǐng)PMSG_BR_ENTRY被廣播到網(wǎng)絡(luò)中,向所有在線的用戶提示一個(gè)新用戶的到達(dá)(即表示“我來了”);所有在線用戶將把該新上線用戶添加到自己的用戶列表中,并向該新上線用戶發(fā)送IPMSG_ANSENTRY命令(即表示“我在線”);該新上線用戶接收到IPMSG_ANSENTRY命令后即將在線用戶添加到自己的用戶列表中。 PS:根據(jù)本系統(tǒng)的特征,可以在聊天部分采用UDP協(xié)議,在文件傳輸,視頻,語音功能上采用TCP協(xié)議 6. 程序啟動(dòng)就要發(fā)送廣播消息,如何發(fā)送廣播消息,以及C#如何實(shí)現(xiàn)廣播. 第一部分.什么是廣播地址,以及廣播地址怎么計(jì)算 1.1廣播地址是什么? 主機(jī)號(hào)全為1,用于向一個(gè)網(wǎng)絡(luò)內(nèi)的所有主機(jī)發(fā)送信息的IP地址.如:受限的廣播地址是255.255.255.255。該地址用于主機(jī)配置過程中IP數(shù)據(jù)報(bào)的目的地址,此時(shí),主機(jī)可能還不知道它所在網(wǎng)絡(luò)的網(wǎng)絡(luò)掩碼,甚至連它的IP地址也不知道。在任何情況下,路由器都不轉(zhuǎn)發(fā)目的地址為受限的廣播地址的數(shù)據(jù)報(bào),這樣的數(shù)據(jù)報(bào)僅出現(xiàn)在本地網(wǎng)絡(luò)中。 PS:一般無特殊要求廣播地址選擇255.255.255.255即可. 1.2計(jì)算方法 首先計(jì)算網(wǎng)絡(luò)地址=IP地址邏輯與(&)子網(wǎng)掩碼 先把IP,子網(wǎng)掩碼轉(zhuǎn)為2進(jìn)制,然后進(jìn)行邏輯與運(yùn)算,得出網(wǎng)絡(luò)地址 例: IP192.168.1.3子網(wǎng)掩碼255.255.0.0 IP轉(zhuǎn)二進(jìn)制11000000.10100100.00000001.00000011 子網(wǎng)掩碼11111111.11111111.00000000.00000000 與運(yùn)算后11000000.10100100.00000000.00000000 192.168.0.0這就是網(wǎng)絡(luò)地址,其中子網(wǎng)掩碼全1對(duì)應(yīng)為網(wǎng)絡(luò)號(hào),全0對(duì)應(yīng)的是主機(jī)號(hào),即192.168.0.0對(duì)應(yīng)的網(wǎng)絡(luò)號(hào)為192.168,主機(jī)號(hào)為0.0.將網(wǎng)絡(luò)地址主機(jī)部分全取反后得到的地址便是廣播地址: 廣播地址11000000.10100100.11111111.11111111 換成10進(jìn)制則為192.168.0.0 第二部分.C#利用UDP協(xié)議如何實(shí)現(xiàn)廣播 2.1如何實(shí)現(xiàn)UDP廣播,直接舉例說明: button1_Click時(shí)使用了UDP廣播向外發(fā)送了數(shù)據(jù) RecData()在后臺(tái)接受UDP協(xié)議的消息 //UDP通過廣播實(shí)現(xiàn)群發(fā)功能 namespace BroadcastExample { public partial class Form1:Form { delegate void AppendStringCallback(stringtext); AppendStringCallback appendstringcallback; //使用的接收端口51008 ///<summary> ///端口號(hào) ///</summary> private int port=51008; ///<summary> ///udp連接對(duì)象 ///</summary> private UdpClient udpclient; public Form1() { InitializeComponent(); appendstringcallback = new AppendStringCallback(AppendString); } ///<summary> ///委托對(duì)象的處理過程 ///</summary> ///<paramname="text"></param> private void AppendString(stringtext) { if(richtextBox2.InvokeRequired==true) { this.Invoke(appendstringcallback,text); } else { richtextBox2.AppendText(text+"\r\n"); } } ///<summary> ///在后臺(tái)運(yùn)行的接收線程 ///</summary> private void RecData() { //本機(jī)指定端口接收 udpclient=new UdpClient(port); IPEndPoint remote=null; //接收從遠(yuǎn)程主機(jī)發(fā)送過來的信息 while(true) { try { //關(guān)閉udpclient時(shí)此句會(huì)產(chǎn)生異常 byte[]bytes=udpclient.Receive(refremote); stringstr=Encoding.UTF8.GetString(bytes,0,bytes.Length); AppendString(string.Format("來自{0}:{1}",remote,str)); } catch { //退出循環(huán),結(jié)束線程 break; } } } privatevoidForm1_Load(objectsender,EventArgse) { //創(chuàng)建一個(gè)線程接收接收遠(yuǎn)程主機(jī)發(fā)來的信息 Thread mythread=new Thread(new ThreadStart(RecData)); //將線程設(shè)為后臺(tái)運(yùn)行 mythread.IsBackground=true; mythread.Start(); } private void Form1_FormClosing(objectsender,FormClosingEventArgse) { udpclient.Close(); } private void button1_Click(objectsender,EventArgse) { UdpClient myUdpclient=newUdpClient(); try { IPEndPoint iep=new IPEndPoint(IPAddress.Broadcast,port); byte[]bytes=System.Text.Encoding.UTF8.GetBytes(textBox1.Text); myUdpclient.Send(bytes,bytes.Length,iep); textBox1.Clear(); myUdpclient.Close(); textBox1.Focus(); } catch(Exceptionerr) { MessageBox.Show(err.Message,"發(fā)送失敗"); } finally { myUdpclient.Close(); } } } } 啟動(dòng)主程序時(shí),同時(shí)啟動(dòng)UDP的監(jiān)聽,這時(shí)應(yīng)該使用集合來做為消息隊(duì)列的緩存,以便用戶能在任何時(shí)候?yàn)g覽到消息.這個(gè)集合一般在主程序中定義,而用戶接受消息,一般我們會(huì)彈出窗口給用戶來瀏覽消息,以及在新窗口中回復(fù)消息,那如何將主窗口中的消息,傳遞到消息顯示窗體中呢? 如何是Web(ASP.net)我們可以封裝到form中傳值,或者request傳值,甚至可以在URL中接參數(shù)直接傳值,而winform中窗體傳值以上方法就都不在能用了. 在windowsform之間傳值,我總結(jié)了有四個(gè)方法:全局變量、屬性、窗體構(gòu)造函數(shù)和delegate。 第一個(gè)全局變量: 這個(gè)最簡(jiǎn)單,只要把變量描述成static就可以了,在form2中直接引用form1的變量,代碼如下: 在form1中定義一個(gè)static變量publicstaticinti=9; Form2中的鈕扣按鈕如下: privatevoidbutton1_Click(objectsender,System.EventArgse) { textBox1.Text=Form1.i.ToString(); } 第二個(gè)方法是利用屬性: 假設(shè)我們需要點(diǎn)擊主窗體FMMain中的某一個(gè)按鈕時(shí)打開子窗體FMChild并將某一個(gè)值傳給子窗體FMChild,一般情況下,我們點(diǎn)擊按鈕顯示子窗體FMChild的代碼為: FMChildfmChild=newFMChild();fmChild.ShowDialog();fmChild.Dispose(); 如果我們需要將主窗體FMMain中的stringstrValueA的值傳給FMChild,那么我們首先對(duì)strValueA進(jìn)行如下處理: privatestringstrValueA;publicstringStrValueA{get{returnstrValueA;}set{strValueA=value;}} 使其成為主窗體FMMain的一個(gè)屬性,接著修改顯示子窗體的代碼為以下兩種的其中一種。 方法一: FMChildfmChild=newFMChild();fmChild.ShowDialog(this);fmChild.Dispose(); 方法二: FMChildfmChild=newFMChild();FMChild.Owner=this;fmChild.ShowDialog();fmChild.Dispose(); 然后在修改子窗體FMChild中申明一個(gè)主窗體FMMain對(duì)象, FMMainfmMain; 在需要使用主窗體FMMain的stringstrValueA的地方加上如下代碼: fmMain=(FMMain)this.Owner; 這樣,就可以獲得主窗體FMMain中strValueA的值了。 這時(shí),如果你需要將子窗體FMChild中的stringstrValueB傳給主窗體FMMain,同樣處理stringstrValueB. privatestringstrValueB;publicstringStrValueB{get{returnstrValueB;}set{strValueB=value;}} 那么你在關(guān)閉子窗體代碼fmChild.Dispose();后,可以寫一些代碼來保存或者處理FMChild的strValueB,例如: stringstrTmp=fmChild.StrValueB; 第三個(gè)方法是用構(gòu)造函數(shù): Form1的button按鈕這樣寫: privatevoidbutton1_Click(objectsender,System.EventArgse) { Form2temp=newForm2(9); temp.Show(); } Form2的構(gòu)造函數(shù)這樣寫: publicForm2(inti) { InitializeComponent(); textBox1.Text=i.ToString(); } 第四個(gè)方法是用delegate,代碼如下: Form2中先定義一個(gè)delegate publicdelegatevoidreturnvalue(inti); publicreturnvalueReturnValue; form2中的button按鈕代碼如下: privatevoidbutton1_Click(objectsender,System.EventArgse) { if(ReturnValue!=null) ReturnValue(8); } Form1中的button按鍵如下: privatevoidbutton1_Click(objectsender,System.EventArgse) { Form2temp=newForm2(); temp.ReturnValue=newtemp.Form2.returnvalue(showvalue); temp.Show(); } privatevoidshowvalue(inti) { textBox1.Text=i.ToString(); } 點(diǎn)擊form2的button,form1中的textbox中的值就會(huì)相應(yīng)變化。 在這四個(gè)方法中, 第一個(gè)是雙向傳值,也就是說,form1和form2改變i的值,另一方也會(huì)受到影響。 第二個(gè)方法可以單向也可以雙向傳值。 第三個(gè)方法是form1->form2單向傳值。 第四個(gè)方法是form2->form1單向傳值。 現(xiàn)在很多程序都有托盤功能,而我們的聊天工具更是如此,無論是QQ,旺旺,飛鴿傳書等等,都是 以托盤的形式工作在后臺(tái),對(duì)消息進(jìn)行監(jiān)聽的.而VS2005給我們提供了現(xiàn)成的控件,來完成托盤的功能,下面我結(jié)合代碼講解下項(xiàng)目中可能用到的托盤技巧. 1.如何實(shí)現(xiàn)托盤功能: 在VS2005中直接添加notifyIcon控件,然后設(shè)置下icon屬性,給其設(shè)置個(gè)圖標(biāo)即可,使用托盤功能. 但是托盤并不能實(shí)現(xiàn)我們要求的功能,具體的功能實(shí)現(xiàn),需要我們手工添加代碼實(shí)現(xiàn). 2.如何最小化時(shí)自動(dòng)到托盤 private void Form1_Resize(objectsender,System.EventArgse) { if(this.WindowState==FormWindowState.Minimized) { this.Visible=false; this.notifyIcon1.Visible=true; } } 3.如何雙擊托盤恢復(fù)原狀 private void notifyIcon1_Click(objectsender,System.EventArgse) { this.Visible=true; this.WindowState=FormWindowState.Normal; this.notifyIcon1.Visible=false; } 4.實(shí)現(xiàn)托盤的閃爍功能(如QQ有消息時(shí)的閃爍) (1).首先我們?cè)诳瞻状绑w中拖入一個(gè)NotifyIcon控件和定時(shí)控件 privateSystem.Windows.Forms.NotifyIconnotifyIcon1; privateSystem.Windows.Forms.Timertimer1; (2).其次,我們準(zhǔn)備兩張ico圖片,用來顯示在任務(wù)欄,其中一張可用透明的ico圖片,分別叫做1.ico和2.ico;并且建立兩個(gè)icon對(duì)象分別用來存放兩個(gè)ico圖片; privateIconico1=newIcon("1.ico"); privateIconico2=newIcon("2.ICO");//透明的圖標(biāo) (3).在Form_load中初始化notifyicon: privatevoidForm1_Load(objectsender,System.EventArgse) { this.notifyIcon1.Icon=ico1;//設(shè)置程序剛運(yùn)行時(shí)顯示在任務(wù)欄的圖標(biāo) this.timer1.Enable=true;//將定時(shí)控件設(shè)為啟用,默認(rèn)為false; } (4).先設(shè)置一個(gè)全局變量i,用來控制圖片索引,然后創(chuàng)建定時(shí)事件,雙擊定時(shí)控件就可以編輯 inti=0; privatevoidtimer1_Tick(objectsender,System.EventArgse) { //如果i=0則讓任務(wù)欄圖標(biāo)變?yōu)橥该鞯膱D標(biāo)并且退出 if(i<1) { this.notifyIcon1.Icon=ico2; i++; return; } //如果i!=0,就讓任務(wù)欄圖標(biāo)變?yōu)閕co1,并將i置為0; else i=0; } 由于消息傳輸要求較低,而且為了簡(jiǎn)化聊天的步驟,在局域網(wǎng)聊天中,采用UDP是非常好的選擇.因?yàn)閁DP可以不用連接,在獲取用戶列表后,直接點(diǎn)擊用戶名就可以發(fā)送消息,減少了等待連接等繁瑣 1.UDP發(fā)送信息 namespaceXChat.SendMes { public class MsgSend { private UdpClient udp=null; private int PORT; private IPEndPoint endP=null; public MsgSend() { this.PORT=58888; } publicMsgSend(intport) { this.PORT=port; } ///<summary> ///發(fā)送信息 ///</summary> ///<paramname="hostName">要發(fā)送到的主機(jī)名</param> ///<paramname="message">要發(fā)送的信息</param> publicvoidSendMessage(stringhostName,stringmessage) { this.udp=newUdpClient(); endP=newIPEndPoint(Dns.GetHostEntry(hostName).AddressList[0],PORT); try { //byte[]b=Encoding.ASCII.GetBytes(hostName); byte[]b=Encoding.UTF8.GetBytes(message); udp.Send(b,b.Length,endP); } catch { System.Windows.Forms.MessageBox.Show("發(fā)送出錯(cuò)!"); } finally { this.udp.Close(); } } } } 要使用時(shí)直接new MsgSend().SendMessage(主機(jī)名,消息); 2.UDP接收消息 namespaceXChat.SendMes { //設(shè)置消息到前臺(tái)的委托 public delegate voidSet Message(stringmes); public class MsgRecive { private int PORT; private UdpClient udp=null; private Thread recThread=null; private IPEndPoint ipep=null; private SetMessages etMes; public MsgRecive() { this.PORT=58888; } publicMsgRecive(intport) { this.PORT=port; } ///<summary> ///開啟后臺(tái)接受消息線程 ///</summary> ///<paramname="setMes">傳入設(shè)置消息的委托</param> publicvoidStartReciveMsg(SetMessagesetMes) { this.setMes=setMes; udp=newUdpClient(PORT); recThread=newThread(newThreadStart(ReciveMsg)); recThread.Start(); } ///<summary> ///關(guān)閉后臺(tái)消息接收線程 ///</summary> publicvoidCloseReciveMsg() { recThread.Abort(); //recThread.Join(); udp.Close(); } privatevoidReciveMsg() { while(true) //這句很重要,否則CPU很容易100% Thread.Sleep(500); byte[] b = udp.Receive(refipep); string message=Encoding.UTF8.GetString(b,0,b.Length); this.setMes(message); } } } } 在前臺(tái)private MsgRecive mr=null; public xchatFrm() { ?? this.mr=newMsgRecive(port); this.mr.StartReciveMsg(newSetMessage(GetMes)); ?? } 這幾天一直想寫一個(gè)類似QQ文件發(fā)送的東西,上網(wǎng)找了一些資料,都不是很理想,下面我把我的思路和基本實(shí)現(xiàn)代碼說下。 為了把問題說清楚,把一些變量都直接附值了,并沒有通過輸入附值 private string path = "F:\\SmartMovie.EXE"; //要發(fā)送的文件 private Socket s; private void listen() { string ip = "127.0.0.1"; //遠(yuǎn)程IP 這里定義為自己的機(jī)器 IPAddress[] ih = Dns.GetHostAddresses(ip); //獲得IP列表 IPAddress newip = ih[0]; //獲取IP地址 int port = 6789; //定義端口 IPEndPoint Conncet = new IPEndPoint(newip, port); //構(gòu)造結(jié)點(diǎn) s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //初始化socket try { s.Connect(Conncet); //連接遠(yuǎn)程服務(wù)器 if (s.Connected) //如果連接成功 s.Connected 則為true 否則為 false { Thread t = new Thread(new ThreadStart(set)); //創(chuàng)建進(jìn)程 t.Start(); //開始進(jìn)程 Console.WriteLine("發(fā)送完畢") } } catch(NullReferenceException e) { Console.WriteLine("{0}",e); } private void set() //創(chuàng)建set函數(shù) { Console.WriteLine("開始發(fā)送數(shù)據(jù)"); byte[] b = new byte[10000000]; //創(chuàng)建文件緩沖區(qū),這里可以認(rèn)為文件的最大值 FileStream file = File.Open(path, FileMode.Open,FileAccess.Read); //創(chuàng)建文件流 int start = 0; int end = (int)file.Length; //獲取文件長(zhǎng)度 文件傳送如果有需要超過int的范圍估計(jì)就要改寫FileStream類了 try { while (end != 0) { int count = file.Read(b, start, end); //把數(shù)據(jù)寫進(jìn)流 start += count; end -= count; } while (start != 0) { int n = s.Send(b, end, start, SocketFlags.None); //用Socket的Send方法發(fā)送流 end += n; start -= n; } file.Close(); //關(guān)閉文件流 s.Close(); //關(guān)閉Socket } catch (NullReferenceException e) { Console.WriteLine("{0}", e); } 這樣文件發(fā)送的模型就實(shí)現(xiàn)了 接下去實(shí)現(xiàn)文件的接收,首先要確定對(duì)方發(fā)送文件的長(zhǎng)度,其實(shí)上面的那段還要加入發(fā)送文件長(zhǎng)度的功能,實(shí)現(xiàn)很簡(jiǎn)單,就是發(fā)送int變量end ,然后要求接收代碼返回一個(gè)Boolean確定是否發(fā)送,這里為了更簡(jiǎn)明的說清楚原理并沒有實(shí)現(xiàn) private void get() { string path = "G:\\da.exe"; //接收的文件 FileStream file = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write); //寫入文件流 TcpListener listen = new TcpListener(6789); //監(jiān)聽端口 Socket s1 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //定義Socket并初始化 try { listen.Start(); //開始監(jiān)聽 s1 = listen.AcceptSocket(); //獲取Socket連接 byte[] data = new byte[10000000]; //定義緩沖區(qū) int longer = data.Length; int start = 0; int mid = 0; if (s1.Connected) //確定連接 { Console.WriteLine("連接成功"); int count = s1.Receive(data, start, longer, SocketFlags.None); //把接收到的byte存入緩沖區(qū) mid += count; longer -= mid; while (count != 0) { count = s1.Receive(data, mid, longer, SocketFlags.None); mid += count; longer -= mid; } file.Write(data, 0, 1214134); //寫入文件,1214134為文件大小,可以用socket發(fā)送獲得,代碼前面已經(jīng)說明。 s1.Close(); file.Close(); } } catch(NullReferenceException e) { Console.WriteLine("{0}",e); } } 該文章在 2016/12/23 18:45:29 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |