還記得剛剛開始接觸編程開發時,傻傻的將網站開發和網路編程混為一談,常常因分不清楚而引為笑柄。後來勉強分清楚,又因為各種各樣的協議埠之類的名詞而倍感神秘,所以為了揭開網路編程的神秘面紗,本文嘗試以一個簡單的小例子,簡述在網路編程開發中涉及到的相關知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
還記得剛剛開始接觸編程開發時,傻傻的將網站開發和網路編程混為一談,常常因分不清楚而引為笑柄。後來勉強分清楚,又因為各種各樣的協議埠之類的名詞而倍感神秘,所以為了揭開網路編程的神秘面紗,本文嘗試以一個簡單的小例子,簡述在網路編程開發中涉及到的相關知識點,僅供學習分享使用,如有不足之處,還請指正。
概述
在TCP/IP協議族中,傳輸層主要包括TCP和UDP兩種通信協議,它們以不同的方式實現兩台主機中的不同應用程式之間的數據傳輸,即數據的端到端傳輸。由於它們的實現方式不同,因此各有一套屬於自己的埠號,且相互獨立。採用五元組(協議,信源機IP地址,信源應用進程埠,信宿機IP地址,信宿應用進程埠)來描述兩個應用進程之間的通信關聯,這也是進行網路程式設計最基本的概念。傳輸控制協議(Transmission Control Protocol,TCP)提供一種面向連接的、可靠的數據傳輸服務,保證了端到端數據傳輸的可靠性。
涉及知識點
本例中涉及知識點如下所示:
- TcpClient : TcpClient類為TCP網路服務提供客戶端連接,它構建於Socket類之上,以提供較高級別的TCP服務,提供了通過網路連接、發送和接收數據的簡單方法。
- TcpListener:構建於Socket之上,提供了更高抽象級別的TCP服務,使得程式員能更方便地編寫伺服器端應用程式。通常情況下,伺服器端應用程式在啟動時將首先綁定本地網路介面的IP地址和埠號,然後進入偵聽客戶請求的狀態,以便於客戶端應用程式提出顯式請求。
- NetworkStream:提供網路訪問的基礎數據流。一旦偵聽到有客戶端應用程式請求連接偵聽埠,伺服器端應用將接受請求,並建立一個負責與客戶端應用程式通信的通道。
網路聊天示意圖
如下圖所示:看似兩個在不同網路上的人聊天,實際上都是通過服務端進行接收轉發的。
TCP網路通信示意圖
如下圖所示:首先是服務端進行監聽,當有客戶端進行連接時,則建立通訊通道進行通信。
示例截圖
服務端截圖,如下所示:
客戶端截圖,如下所示:開啟兩個客戶端,開始美猴王和二師兄的對話。
核心代碼
發送信息類,如下所示:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Common 8 { 9 /// <summary> 10 /// 定義一個類,所有要發送的內容,都按照這個來 11 /// </summary> 12 public class ChatMessage 13 { 14 /// <summary> 15 /// 頭部信息 16 /// </summary> 17 public ChatHeader header { get; set; } 18 19 /// <summary> 20 /// 信息類型,預設為文本 21 /// </summary> 22 public ChatType chatType { get; set; } 23 24 /// <summary> 25 /// 內容信息 26 /// </summary> 27 public string info { get; set; } 28 29 } 30 31 /// <summary> 32 /// 頭部信息 33 /// </summary> 34 public class ChatHeader 35 { 36 /// <summary> 37 /// id唯一標識 38 /// </summary> 39 public string id { get; set; } 40 41 /// <summary> 42 /// 源:發送方 43 /// </summary> 44 public string source { get; set; } 45 46 /// <summary> 47 /// 目標:接收方 48 /// </summary> 49 public string dest { get; set; } 50 51 } 52 53 /// <summary> 54 /// 內容標識 55 /// </summary> 56 public enum ChatMark 57 { 58 BEGIN = 0x0000, 59 END = 0xFFFF 60 } 61 62 public enum ChatType { 63 TEXT=0, 64 IMAGE=1 65 } 66 }View Code
打包幫助類,如下所示:所有需要發送的信息,都要進行封裝,打包,編碼成固定格式,方便解析。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Common 8 { 9 /// <summary> 10 /// 包幫助類 11 /// </summary> 12 public class PackHelper 13 { 14 /// <summary> 15 /// 獲取待發送的信息 16 /// </summary> 17 /// <param name="text"></param> 18 /// <returns></returns> 19 public static byte[] GetSendMsgBytes(string text, string source, string dest) 20 { 21 ChatHeader header = new ChatHeader() 22 { 23 source = source, 24 dest = dest, 25 id = Guid.NewGuid().ToString() 26 }; 27 ChatMessage msg = new ChatMessage() 28 { 29 chatType = ChatType.TEXT, 30 header = header, 31 info = text 32 }; 33 string msg01 = GeneratePack<ChatMessage>(msg); 34 byte[] buffer = Encoding.UTF8.GetBytes(msg01); 35 return buffer; 36 } 37 38 /// <summary> 39 /// 生成要發送的包 40 /// </summary> 41 /// <typeparam name="T"></typeparam> 42 /// <param name="t"></param> 43 /// <returns></returns> 44 public static string GeneratePack<T>(T t) { 45 string send = SerializerHelper.JsonSerialize<T>(t); 46 string res = string.Format("{0}|{1}|{2}",ChatMark.BEGIN.ToString("X").PadLeft(4, '0'), send, ChatMark.END.ToString("X").PadLeft(4, '0')); 47 int length = res.Length; 48 49 return string.Format("{0}|{1}", length.ToString().PadLeft(4, '0'), res); 50 } 51 52 /// <summary> 53 /// 解析包 54 /// </summary> 55 /// <typeparam name="T"></typeparam> 56 /// <param name="receive">原始接收數據包</param> 57 /// <returns></returns> 58 public static T ParsePack<T>(string msg, out string error) 59 { 60 error = string.Empty; 61 int len = int.Parse(msg.Substring(0, 4));//傳輸內容的長度 62 string msg2 = msg.Substring(msg.IndexOf("|") + 1); 63 string[] array = msg2.Split('|'); 64 if (msg2.Length == len) 65 { 66 string receive = array[1]; 67 string begin = array[0]; 68 string end = array[2]; 69 if (begin == ChatMark.BEGIN.ToString("X").PadLeft(4, '0') && end == ChatMark.END.ToString("X").PadLeft(4, '0')) 70 { 71 T t = SerializerHelper.JsonDeserialize<T>(receive); 72 if (t != null) 73 { 74 return t; 75 76 } 77 else { 78 error = string.Format("接收的數據有誤,無法進行解析"); 79 return default(T); 80 } 81 } 82 else { 83 error = string.Format("接收的數據格式有誤,無法進行解析"); 84 return default(T); 85 } 86 } 87 else { 88 error = string.Format("接收數據失敗,長度不匹配,定義長度{0},實際長度{1}", len, msg2.Length); 89 return default(T); 90 } 91 } 92 } 93 }View Code
服務端類,如下所示:服務端開啟時,需要進行埠監聽,等待鏈接。
1 using Common; 2 using System; 3 using System.Collections.Generic; 4 using System.Configuration; 5 using System.IO; 6 using System.Linq; 7 using System.Net; 8 using System.Net.Sockets; 9 using System.Text; 10 using System.Threading; 11 using System.Threading.Tasks; 12 13 /// <summary> 14 /// 描述:MeChat服務端,用於接收數據 15 /// </summary> 16 namespace MeChatServer 17 { 18 public class Program 19 { 20 /// <summary> 21 /// 服務端IP 22 /// </summary> 23 private static string IP; 24 25 /// <summary> 26 /// 服務埠 27 /// </summary> 28 private static int PORT; 29 30 /// <summary> 31 /// 服務端監聽 32 /// </summary> 33 private static TcpListener tcpListener; 34 35 36 public static void Main(string[] args) 37 { 38 //初始化信息 39 InitInfo(); 40 IPAddress ipAddr = IPAddress.Parse(IP); 41 tcpListener = new TcpListener(ipAddr, PORT); 42 tcpListener.Start(); 43 44 Console.WriteLine("等待連接"); 45 tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async"); 46 //如果用戶按下Esc鍵,則結束 47 while (Console.ReadKey().Key != ConsoleKey.Escape) 48 { 49 Thread.Sleep(200); 50 } 51 tcpListener.Stop(); 52 } 53 54 /// <summary> 55 /// 初始化信息 56 /// </summary> 57 private static void InitInfo() { 58 //初始化服務IP和埠 59 IP = ConfigurationManager.AppSettings["ip"]; 60 PORT = int.Parse(ConfigurationManager.AppSettings["port"]); 61 //初始化數據池 62 PackPool.ToSendList = new List<ChatMessage>(); 63 PackPool.HaveSendList = new List<ChatMessage>(); 64 PackPool.obj = new object(); 65 } 66 67 /// <summary> 68 /// Tcp非同步接收函數 69 /// </summary> 70 /// <param name="ar"></param> 71 public static void AsyncTcpCallback(IAsyncResult ar) { 72 Console.WriteLine("已經連接"); 73 ChatLinker linker = new ChatLinker(tcpListener.EndAcceptTcpClient(ar)); 74 linker.BeginRead(); 75 //繼續下一個連接 76 Console.WriteLine("等待連接"); 77 tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async"); 78 } 79 } 80 }View Code
客戶端類,如下所示:客戶端主要進行數據的封裝發送,接收解析等操作,併在頁面關閉時,關閉連接。
1 using Common; 2 using System; 3 using System.Collections.Generic; 4 using System.ComponentModel; 5 using System.Data; 6 using System.Drawing; 7 using System.Linq; 8 using System.Net.Sockets; 9 using System.Text; 10 using System.Threading; 11 using System.Threading.Tasks; 12 using System.Windows.Forms; 13 14 namespace MeChatClient 15 { 16 /// <summary> 17 /// 聊天頁面 18 /// </summary> 19 public partial class FrmMain : Form 20 { 21 /// <summary> 22 /// 鏈接客戶端 23 /// </summary> 24 private TcpClient tcpClient; 25 26 /// <summary> 27 /// 基礎訪問的數據流 28 /// </summary> 29 private NetworkStream stream; 30 31 /// <summary> 32 /// 讀取的緩衝數組 33 /// </summary> 34 private byte[] bufferRead; 35 36 /// <summary> 37 /// 昵稱信息 38 /// </summary> 39 private Dictionary<string, string> dicNickInfo; 40 41 public FrmMain() 42 { 43 InitializeComponent(); 44 } 45 46 private void MainForm_Load(object sender, EventArgs e) 47 { 48 //獲取昵稱 49 dicNickInfo = ChatInfo.GetNickInfo(); 50 //設置標題 51 string title = string.Format(":{0}-->{1} 的對話",dicNickInfo[ChatInfo.Source], dicNickInfo[ChatInfo.Dest]); 52 this.Text = string.Format("{0}:{1}", this.Text, title); 53 //初始化客戶端連接 54 this.tcpClient = new TcpClient(AddressFamily.InterNetwork); 55 bufferRead = new byte[this.tcpClient.ReceiveBufferSize]; 56 this.tcpClient.BeginConnect(ChatInfo.IP, ChatInfo.PORT, new AsyncCallback(RequestCallback), null); 57 58 } 59 60 /// <summary> 61 /// 非同步請求鏈接函數 62 /// </summary> 63 /// <param name="ar"></param> 64 private void RequestCallback(IAsyncResult ar) { 65 this.tcpClient.EndConnect(ar); 66 this.lblStatus.Text = "連接伺服器成功"; 67 //獲取流 68 stream = this.tcpClient.GetStream(); 69 //先發送一個連接信息 70 string text = CommonVar.LOGIN; 71 byte[] buffer = PackHelper.GetSendMsgBytes(text,ChatInfo.Source,ChatInfo.Source); 72 stream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteMessage), null); 73 //只有stream不為空的時候才可以讀 74 stream.BeginRead(bufferRead, 0, bufferRead.Length, new AsyncCallback(ReadMessage), null); 75 } 76 77 /// <summary> 78 /// 發送信息 79 /// </summary> 80 /// <param name="sender"></param> 81 /// <param name="e"></param> 82 private void btnSend_Click(object sender, EventArgs e) 83 { 84 string text = this.txtMsg.Text.Trim(); 85 if( string.IsNullOrEmpty(text)){ 86 MessageBox.Show("要發送的信息為空"); 87 return; 88 } 89 byte[] buffer = ChatInfo.GetSendMsgBytes(text); 90 stream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteMessage), null); 91 this.rtAllMsg.AppendText(string.Format("\r\n[{0}]", dicNickInfo[ChatInfo.Source])); 92 this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right; 93 this.rtAllMsg.AppendText(string.Format("\r\n{0}", text)); 94 this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right; 95 } 96 97 98 /// <summary> 99 /// 非同步讀取信息 100 /// </summary> 101 /// <param name="ar"></param> 102 private void ReadMessage(IAsyncResult ar) 103 { 104 if (stream.CanRead) 105 { 106 int length = stream.EndRead(ar); 107 if (length >= 1) 108 { 109 110 string msg = string.Empty; 111 msg = string.Concat(msg, Encoding.UTF8.GetString(bufferRead, 0, length)); 112 //處理接收的數據 113 string error = string.Empty; 114 ChatMessage t = PackHelper.ParsePack<ChatMessage>(msg, out error); 115 if (string.IsNullOrEmpty(error)) 116 { 117 this.rtAllMsg.Invoke(new Action(() => 118 { 119 this.rtAllMsg.AppendText(string.Format("\r\n[{0}]", dicNickInfo[t.header.source])); 120 this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left; 121 this.rtAllMsg.AppendText(string.Format("\r\n{0}", t.info)); 122 this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left; 123 this.lblStatus.Text = "接收數據成功!"; 124 })); 125 } 126 else { 127 this.lblStatus.Text = "接收數據失敗:"+error; 128 } 129 } 130 //繼續讀數據 131 stream.BeginRead(bufferRead, 0, bufferRead.Length, new AsyncCallback(ReadMessage), null); 132 } 133 } 134 135 /// <summary> 136 /// 發送成功 137 /// </summary> 138 /// <param name="ar"></param> 139 private void WriteMessage(IAsyncResult ar) 140 { 141 this.stream.EndWrite(ar); 142 //發送成功 143 } 144 145 /// <summary> 146 /// 頁面關閉,