本文講下C#通過Socket編程實現平行主機之間網路通訊的詳細講解,非常不錯的博文,一起來看下吧。
在程式設計中,涉及資料存放區和資料交換的時候,不管是B/S還是C/S模式 ,都有這樣一個概念:資料庫伺服器。這要求一台效能和配置都比較好的主機作為伺服器,以滿足數目眾多的用戶端進行頻繁訪問。但是對於一些資料交換的要求不主同,而且涉及到的通訊個體數目不多,如果還採用“一主機多客戶機”的模式,便要求一台硬體設定良好而且軟體上安裝了相關資料服務軟體,這樣會造成硬體和軟體上的很多不必要的成本,這時Socket在點對點的平行對象之間的網路通訊的優勢就就發揮出來了。
其實對於Socket通訊來說,伺服器和用戶端的界定不像資料庫伺服器與用戶端那樣明顯,甚至可以說Socket通訊裡面的伺服器和用戶端只是相對的,因為網路通訊的對象基本上是處於平等層面的,只是為了方便對兩台連網通訊的主機的描述才這樣定義稱謂的。
由於在.NET中Socket通訊的建立很容易,所以本文主要介紹一個Socket的比較典型的應用的流程:用戶端向伺服器發送圖片請求,圖片伺服器接收到請求,並將伺服器硬碟上的圖片編碼,發送到用戶端,用戶端得到圖片資料後,再將這些資料寫成圖片檔案,儲存在用戶端上。
本文主要是對Socket的一個應用進行介紹,所以至於其原理在此沒有深究,至於如何建立Socket還有如何?網路的七層協議在此都沒有進行相關研究和介紹,本文主要介紹如何?一個使用者想要的功能,即在兩台主機之間進行通訊,通過網路來收發使用者想要收發的資料。
一、通訊流程圖
二、通訊相關的代碼
本文以Windows控制台程式為例來實現引功能。
不管是通訊伺服器或者通訊用戶端,本文均以一個不斷啟動並執行線程來實現對連接埠的偵聽,將通訊相關的變數的函數做成一個類,在Program.cs中只負責初始化一些參數,然後建立通訊的線程。具體代碼如下:
2.1伺服器端
Program.cs:
using System;using System.Net;using System.Net.Sockets;using System.Threading;namespace ConsoleSocketsDemo{ class Program { static void Main(string[] args) { int sendPicPort = 600;//發送圖片的連接埠 int recvCmdPort = 400;//接收請求的連接埠開啟後就一直進行偵聽 SocketServer socketServerProcess = new SocketServer(recvCmdPort, sendPicPort); Thread tSocketServer = new Thread(new ThreadStart(socketServerProcess.thread));//線程開始的時候要調用的方法為threadProc.thread tSocketServer.IsBackground = true;//設定IsBackground=true,後台線程會自動根據主線程的銷毀而銷毀 tSocketServer.Start(); Console.ReadKey();//直接main裡邊最後加個Console.Read()不就好了。要按鍵才退出。 } }}
SocketServer.cs:
using System;using System.Text;using System.Net;using System.Net.Sockets;using System.IO;namespace ConsoleSocketsDemo{ class SocketServer { Socket sRecvCmd; int recvCmdPort;//接收圖片請求命令 int sendPicPort;//發送圖片命令 public SocketServer(int recvPort,int sendPort) { recvCmdPort = recvPort; sendPicPort = sendPort; //建立本地socket,一直對4000連接埠進行偵聽 IPEndPoint recvCmdLocalEndPoint = new IPEndPoint(IPAddress.Any, recvCmdPort); sRecvCmd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sRecvCmd.Bind(recvCmdLocalEndPoint); sRecvCmd.Listen(100); } public void thread() { while (true) { System.Threading.Thread.Sleep(1);//每個線程內部的死迴圈裡面都要加個“短時間”睡眠,使得線程佔用資源得到及時釋放 try { Socket sRecvCmdTemp = sRecvCmd.Accept();//Accept 以同步方式從偵聽通訊端的串連請求隊列中提取第一個掛起的串連請求,然後建立並返回新的 Socket sRecvCmdTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 5000);//設定接收資料逾時 sRecvCmdTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 5000);//設定發送資料逾時 sRecvCmdTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 1024); //設定發送緩衝區大小 1K sRecvCmdTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 1024);//設定接收緩衝區大小1K byte[] recvBytes = new byte[1024];//開啟一個緩衝區,儲存接收到的資訊 sRecvCmdTemp.Receive(recvBytes); //將讀得的內容放在recvBytes中 string strRecvCmd = Encoding.Default.GetString(recvBytes);// //程式運行到這個地方,已經能接收到遠程發過來的命令了 //************* //解碼命令,並執行相應的操作----如下面的發送本機圖片 //************* string[] strArray = strRecvCmd.Split(';'); if (strArray[0] == "PicRequest") { string[] strRemoteEndPoint = sRecvCmdTemp.RemoteEndPoint.ToString().Split(':');//遠處終端的請求端IP和連接埠,如:127.0.0.1:4000 string strRemoteIP = strRemoteEndPoint[0]; SentPictures(strRemoteIP, sendPicPort); //發送本機圖片檔案 recvBytes = null; } } catch(Exception ex) { Console.Write(ex.Message); } } } /// <summary> /// 向遠程用戶端發送圖片 /// </summary> /// <param name="strRemoteIP">遠程用戶端IP</param> /// <param name="sendPort">發送圖片的連接埠</param> private static void SentPictures(string strRemoteIP, int sendPort) { string path = "D:\\images\\"; string strImageTag = "image";//圖片名稱中包含有image的所有圖片檔案 try { string[] picFiles = Directory.GetFiles(path, strImageTag + "*", SearchOption.TopDirectoryOnly);//滿足要求的檔案個數 if (picFiles.Length == 0) { return;//沒有圖片,不做處理 } long sendBytesTotalCounts = 0;//發送資料流總長度 //訊息頭部:命令標識+檔案數目+……檔案i長度+ string strMsgHead = "PicResponse;" + picFiles.Length + ";"; //訊息體:圖片檔案流 byte[][] msgPicBytes = new byte[picFiles.Length][]; for (int j = 0; j < picFiles.Length; j++) { FileStream fs = new FileStream(picFiles[j].ToString(), FileMode.Open, FileAccess.Read); BinaryReader reader = new BinaryReader(fs); msgPicBytes[j] = new byte[fs.Length]; strMsgHead += fs.Length.ToString() + ";"; sendBytesTotalCounts += fs.Length; reader.Read(msgPicBytes[j], 0, msgPicBytes[j].Length); } byte[] msgHeadBytes = Encoding.Default.GetBytes(strMsgHead);//將訊息頭字串轉成byte數組 sendBytesTotalCounts += msgHeadBytes.Length; //要發送的資料流:資料頭+資料體 byte[] sendMsgBytes = new byte[sendBytesTotalCounts];//要發送的總數組 for (int i = 0; i < msgHeadBytes.Length; i++) { sendMsgBytes[i] = msgHeadBytes[i]; //資料頭 } int index = msgHeadBytes.Length; for (int i = 0; i < picFiles.Length; i++) { for (int j = 0; j < msgPicBytes[i].Length; j++) { sendMsgBytes[index + j] = msgPicBytes[i][j]; } index += msgPicBytes[i].Length; } //程式執行到此處,帶有圖片資訊的報文已經準備好了 //PicResponse;2;94223;69228; //+圖片1位元流+……圖片2位元流 try { #region 發送圖片 Socket sSendPic = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAddress = IPAddress.Parse(strRemoteIP);//remoteip = "127.0.0.1" try { sSendPic.Connect(ipAddress, sendPort);//串連無端用戶端主機 sSendPic.Send(sendMsgBytes, sendMsgBytes.Length, 0);//發送本地圖片 } catch (System.Exception e) { System.Console.Write("SentPictures函數在建立遠端連線時出現異常:" + e.Message); }finally { sSendPic.Close(); } #endregion } catch { } } catch(Exception ex) { Console.Write(ex.Message); } } }}
2.2用戶端端
Program.cs:
using System;using System.Text;using System.Net;using System.Net.Sockets;using System.Threading;namespace ConsoleClientSocketDemo{ class Program { static void Main(string[] args) { int recvPort = 600;//用戶端一直對600連接埠進行偵聽---接收圖片的連接埠 RecvPic recvPic = new RecvPic(recvPort);//監聽接收來自圖片伺服器的圖片以及用戶端的命令 Thread tRecvPic = new Thread(new ThreadStart(recvPic.thread)); tRecvPic.IsBackground = true; tRecvPic.Start(); string strPicServerIP = "127.0.0.1";//圖片伺服器的IP----127.0.0.1(localhost)--以本機為例 int sendRequestPort = 400;//發送圖片請求的連接埠 SendStrMsg(strPicServerIP, sendRequestPort); Console.ReadKey();//直接main裡邊最後加個Console.Read()不就好了。要按鍵才退出。 } /// <summary> /// 向目標主機發送字串 請求圖片 /// </summary> /// <param name="strPicServerIP">靶心圖表片伺服器IP</param> /// <param name="sendRequestPort">靶心圖表片伺服器接收請求的連接埠</param> private static void SendStrMsg(string strPicServerIP, int sendRequestPort) { //可以在字串編碼上做文章,可以傳送各種資訊內容,目前主要有三種編碼方式: //1.自訂連接字串編碼--微量 //2.JSON編碼--輕量 //3.XML編碼--重量 string strPicRequest = "PicRequest;Hello world,need some pictures~!";//圖片請求 IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse(strPicServerIP.ToString()), sendRequestPort); Socket answerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { answerSocket.Connect(ipEndPoint);//建立Socket串連 byte[] sendContents = Encoding.UTF8.GetBytes(strPicRequest); answerSocket.Send(sendContents, sendContents.Length, 0);//發送位元據 } catch (Exception ex) { Console.Write(ex.Message); } finally { answerSocket.Close(); } } }}RecvPic.cs:
using System;using System.Text;using System.Net;using System.Net.Sockets;using System.IO;namespace ConsoleClientSocketDemo{ class RecvPic { Socket sRecvPic;//接收圖片的socket int recvPicPort;//接收圖片連接埠 public RecvPic(int recvPort) { recvPicPort = recvPort; IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, recvPicPort); sRecvPic = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sRecvPic.Bind(localEndPoint); sRecvPic.Listen(100); } public void thread() { while (true) { System.Threading.Thread.Sleep(1);//每個線程內部的死迴圈裡面都要加個“短時間”睡眠,使得線程佔用資源得到及時釋放 try { Socket sRecvPicTemp = sRecvPic.Accept();//一直在等待socket請求,並建立一個和請求相同的socket,覆蓋掉原來的socket sRecvPicTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 5000); //設定接收資料逾時 sRecvPicTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 5000);//設定發送資料逾時 sRecvPicTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 1024);//設定發送緩衝區大小--1K大小 sRecvPicTemp.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 1024); //設定接收緩衝區大小 #region 先取出資料頭部資訊---並解析頭部 byte[] recvHeadBytes = new byte[1024];//先取1K的資料,提取出資料的頭部 sRecvPicTemp.Receive(recvHeadBytes, recvHeadBytes.Length, 0); string recvStr = Encoding.UTF8.GetString(recvHeadBytes); string[] strHeadArray = recvStr.Split(';');//PicResponse;2;94223;69228; string strHeadCmd = strHeadArray[0];//頭部命令 int picCounts = Convert.ToInt32(strHeadArray[1]) ;//資料流中包含的圖片個數 int[] picLength=new int[picCounts];//每個圖片的長度 for (int i = 0; i < picCounts;i++ ) { picLength[i] = Convert.ToInt32(strHeadArray[i+2]); } #endregion int offset=0;//資料頭的長度 for (int k = 0; k < strHeadArray.Length - 1;k++ ) { offset += strHeadArray[k].Length + 1;//因為後面的分號 } int picOffset = recvHeadBytes.Length - offset;//第一張圖片在提取資料頭的時候已經被提取了一部分了 if (strHeadCmd == "PicResponse") { #region 儲存圖片--為了節約記憶體,可以每接收一次就儲存一次圖片 for (int i = 0; i < picCounts; i++) { byte[] recvPicBytes = new byte[(picLength[i])];//每次只接收一張圖片 if (i == 0)//第一幅圖片有一部分在提取資料頭的時候已經提取過了。 { byte[] recvFirstPicBuffer = new byte[picLength[i] - picOffset]; sRecvPicTemp.Receive(recvFirstPicBuffer, recvFirstPicBuffer.Length, 0); for (int j = 0; j < picOffset; j++) { recvPicBytes[j] = recvHeadBytes[offset + j];//第一幅圖片的前一部分 } for (int j = 0; j < recvFirstPicBuffer.Length; j++)//第一張圖片的後半部分 { recvPicBytes[picOffset + j] = recvFirstPicBuffer[j]; } //將圖片寫入檔案 SavePicture(recvPicBytes, "-0"); } else { sRecvPicTemp.Receive(recvPicBytes, recvPicBytes.Length, 0);//每次取一張圖片的長度 SavePicture(recvPicBytes, "-"+i.ToString()); //將圖片資料寫入檔案 } } #endregion } } catch(Exception ex) { Console.Write(ex.Message); } finally { } } } /// <summary> /// 儲存圖片到指定路徑 /// </summary> /// <param name="picBytes">圖片位元流</param> /// <param name="picNum">圖片編號</param> public void SavePicture(byte[] picBytes, string picNum) { string filename = "receivePic"; if (!Directory.Exists("E:\\images\\")) Directory.CreateDirectory("E:\\images\\"); if (File.Exists("E:\\images\\" + filename + picNum + ".jpg")) return; FileStream fs = new FileStream("E:\\images\\" + filename + picNum + ".jpg", FileMode.OpenOrCreate, FileAccess.Write); fs.Write(picBytes, 0, picBytes.Length); fs.Dispose(); fs.Close(); } }}
三、測試socket的串連方法,telnet遠程登入
使用者可以同時對用戶端和伺服器端的Socket程式進行編寫,然後進行聯調,也可以一次只編寫一個,然後通過下面的方法來測試Socket串連。
一般通過遠程登入來測試連接是否成功,比如測試原生400連接埠是否能串連成功:
“運行->cmd->telnet 127.0.0.1 400”
在沒有運行對原生400連接埠進行不斷偵聽的程式時,會出現串連失敗的提示:
如果串連成功,則會彈出另外一個視窗:
如果在偵聽線程裡面設定斷點,通常串連成功後,就會在
Socket sRecvCmdTemp = sRecvCmd.Accept();
之後的語句上斷點。
附件:SocketDemo.rar
附近示範程式的說明:
1.使用VS2005建立。
2.主要實現的功能是:主機A向主機B發圖片請求,主機B將D盤image目錄下的image0.jpg,image1.jpg檔案編碼發送到主機B,主機B再解碼並寫成圖片檔案到E盤的image目錄下。
3.為了方便調試,示範程式將伺服器和用戶端同時放在本機上,即localhost或者127.0.0.1,即本程式最終實現的效果就是將原生D盤image目錄下的兩個指定名稱的圖片傳送到E盤image目錄下。所以在運行本程式前,先在D:/image目錄下放置兩張命名為image0.jpg,image1.jpg的圖片檔案
4.先運行伺服器程式,再運行用戶端程式