Socket 英文直譯爲「孔或插座」,也稱爲套接字。用於描述 IP 地址和端口號,是一種進程間的通訊機制。你能夠理解爲 IP 地址肯定了網內的惟一計算機,而端口號則指定了將消息發送給哪個應用程序(大多應用程序啓動時會主動綁定一個端口,若是不主動綁定,操做系統自動爲其分配一個端口)。 數組
一臺主機通常運行了多個軟件並同時提供一些服務。每種服務都會打開一個 Socket,並綁定到一個端口號上,不一樣端口對應於不一樣的應用程序。例如 http 使用 80 端口;ftp 使用 21 端口;smtp 使用 23 端口。 安全
上述過程就像是實現了一次三方會談。服務器端的 Socket 至少會有 2 個。一個是 Watch Socket,每成功接收到一個客戶端的鏈接,便在服務器端建立一個通訊 Socket。客戶端 Socket 指定要鏈接的服務器端地址和端口,建立一個 Socket 對象來初始化一個到服務器的 TCP 鏈接。 服務器
下面就看一個最簡單的 Socket 示例,實現了網絡聊天通訊的雛形。 網絡
服務器端:socket
public partial class ChatServer : Form { public ChatServer() { InitializeComponent(); ListBox.CheckForIllegalCrossThreadCalls = false; } /// <summary> /// 監聽 Socket 運行的線程 /// </summary> Thread threadWatch = null; /// <summary> /// 監聽 Socket /// </summary> Socket socketWatch = null; /// <summary> /// 服務器端通訊套接字集合 /// 必須在每次客戶端鏈接成功以後,保存新建的通信套接字,這樣才能和後續的全部客戶端通訊 /// </summary> Dictionary<string, Socket> dictCommunication = new Dictionary<string, Socket>(); /// <summary> /// 通訊線程的集合,用來接收客戶端發送的信息 /// </summary> Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>(); private void btnBeginListen_Click(object sender, EventArgs e) { // 建立服務器端監聽 Socket (IP4尋址協議,流式鏈接,TCP協議傳輸數據) socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 監聽套接字綁定指定端口 IPAddress address = IPAddress.Parse(txtIP.Text.Trim()); IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim())); socketWatch.Bind(endPoint); // 將監聽套接字置於偵聽狀態,並設置鏈接隊列的最大長度 socketWatch.Listen(20); // 啓動監聽線程開始監聽客戶端請求 threadWatch = new Thread(Watch); threadWatch.IsBackground = true; threadWatch.Start(); ShowMsg("服務器啓動完成!"); } Socket socketCommunication = null; private void Watch() { while (true) { // Accept() 會建立新的通訊 Socket,且會阻斷當前線程,所以應置於非主線程上使用 // Accept() 與線程上接受的委託類型不符,所以需另建一方法作橋接 socketCommunication = socketWatch.Accept(); // 將新建的通訊套接字存入集合中,以便服務器隨時能夠向指定客戶端發送消息 // 如不置於集合中,每次 new 出的通訊線程都是一個新的套接字,那麼原套接字將失去引用 dictCommunication.Add(socketCommunication.RemoteEndPoint.ToString(), socketCommunication); lbSocketOnline.Items.Add(socketCommunication.RemoteEndPoint.ToString()); // Receive 也是一個阻塞方法,不能直接運行在 Watch 中,不然監聽線程會阻塞 // 另外,將每個通訊線程存入集合,方便從此的管理(如關閉、或掛起) Thread thread = new Thread(() => { while (true) { byte[] bytes = new byte[1024 * 1024 * 2]; int length = socketCommunication.Receive(bytes); string msg = Encoding.UTF8.GetString(bytes, 0, length); ShowMsg("接收到來自" + socketCommunication.RemoteEndPoint.ToString() + "的數據:" + msg); } }); thread.IsBackground = true; thread.Start(); dictThread.Add(socketCommunication.RemoteEndPoint.ToString(), thread); ShowMsg("客戶端鏈接成功!通訊地址爲:" + socketCommunication.RemoteEndPoint.ToString()); } } delegate void ShowMsgCallback(string msg); private void ShowMsg(string msg) { if (this.InvokeRequired) // 也能夠啓動時修改控件的 CheckForIllegalCrossThreadCalls 屬性 { this.Invoke(new ShowMsgCallback(ShowMsg), new object[] { msg }); } else { this.txtMsg.AppendText(msg + "\r\n"); } } private void btnSendMsg_Click(object sender, EventArgs e) { if (lbSocketOnline.Text.Length == 0) MessageBox.Show("至少選擇一個客戶端才能發送消息!"); else { // Send() 只接受字節數組 string msg = txtSendMsg.Text.Trim(); dictCommunication[lbSocketOnline.Text].Send(Encoding.UTF8.GetBytes(msg)); ShowMsg("發送數據:" + msg); } } private void btnSendToAll_Click(object sender, EventArgs e) { string msg = txtSendMsg.Text.Trim(); foreach (var socket in dictCommunication.Values) { socket.Send(Encoding.UTF8.GetBytes(msg)); } ShowMsg("羣發數據:" + msg); } }
客戶端:ui
public partial class ChatClient : Form { public ChatClient() { InitializeComponent(); } /// <summary> /// 此線程用來接收服務器發送的數據 /// </summary> Thread threadRecive = null; Socket socketClient = null; private void btnConnect_Click(object sender, EventArgs e) { // 客戶端建立通信套接字並鏈接服務器、開始接收服務器傳來的數據 socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socketClient.Connect(IPAddress.Parse(txtIP.Text.Trim()), int.Parse(txtPort.Text.Trim())); ShowMsg(string.Format("鏈接服務器({0}:{1})成功!", txtIP.Text.Trim(), txtPort.Text.Trim())); threadRecive = new Thread(new ThreadStart(() => { while (true) { // Receive 方法從套接字中接收數據,並存入接收緩衝區 byte[] bytes = new byte[1024 * 1024 * 2]; int length = socketClient.Receive(bytes); string msg = Encoding.UTF8.GetString(bytes, 0, length); ShowMsg("接收到數據:" + msg); } })); threadRecive.IsBackground = true; threadRecive.Start(); } delegate void ShowMsgCallback(string msg); private void ShowMsg(string msg) { if (this.InvokeRequired) // 也能夠啓動時修改控件的 CheckForIllegalCrossThreadCalls 屬性 { this.Invoke(new ShowMsgCallback(ShowMsg), new object[] { msg }); } else { this.txtMsg.AppendText(msg + "\r\n"); } } private void btnSend_Click(object sender, EventArgs e) { string msg = txtSendMsg.Text.Trim(); socketClient.Send(Encoding.UTF8.GetBytes(msg)); ShowMsg("發送數據:" + msg); } }
如今全部客戶都能和服務器進行通訊,服務器也能和全部客戶進行通訊。那麼,客戶端之間互相通訊呢? this
顯然,在客戶端界面也應建立在線列表,每次有人登陸後,服務器端除了刷新自身在線列表外,還需將新客戶端的套接字信息發送給其餘在線客戶端,以便它們更新本身的在線列表。 spa
客戶端發送消息給服務器,服務器轉發此消息給另外一個客戶端。固然,這個消息須要進行一些處理,至少要包含目標套接字和發送內容。 操作系統
更爲完善的是,服務器必須定時按制定的規則檢測列表中套接字通訊的有效性,經過發送響應信號,並接收客戶端應答信號以確認客戶端的鏈接性是真實的(不然,需剔除無效客戶端)。 線程
客戶端:
private void btnChooseFile_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); if (ofd.ShowDialog() == DialogResult.OK) { txtFilePath.Text = ofd.FileName; } } private void btnSendFile_Click(object sender, EventArgs e) { using (FileStream fs = new FileStream(txtFilePath.Text, FileMode.Open)) { byte[] bytes = new byte[1024 * 1024 * 2]; // 假設第一個字節爲標誌位:0 表示傳送文件 // 方式一:總體向後偏移 1 個字節;但這樣有潛在缺點, // 有時在通訊時會很是準確的按照約定的字節長度來傳遞, // 那麼這種偏移方案顯然是不可靠的 // bytes[0] = 0; // int length = fs.Read(bytes, 1, bytes.Length); // 方式二:建立多出 1 個字節的數組發送 int length = fs.Read(bytes, 0, bytes.Length); byte[] newBytes = new byte[length + 1]; newBytes[0] = 0; // BlockCopy() 會比你本身寫for循環賦值更爲簡單合適 Buffer.BlockCopy(bytes, 0, newBytes, 1, length); socketClient.Send(newBytes); } }
服務器端(Receive 方法中修改爲這樣):
Thread thread = new Thread(() => { while (true) { byte[] bytes = new byte[1024 * 1024 * 2]; int length = socketCommunication.Receive(bytes); if (bytes[0] == 0) // File { SaveFileDialog sfd = new SaveFileDialog(); if (sfd.ShowDialog() == DialogResult.OK) { using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create)) { fs.Write(bytes, 1, length - 1); fs.Flush(); ShowMsg("文件保存成功,路徑爲:" + sfd.FileName); } } } else // Msg { string msg = Encoding.UTF8.GetString(bytes, 0, length); ShowMsg("接收到來自" + socketCommunication.RemoteEndPoint.ToString() + "的數據:" + msg); } } });
Socket 通訊屬於網絡通訊程序,會有許多的意外,必須進行異常處理以便程序不會被輕易的擊垮。不論是客戶端仍是服務器端,只要和網絡交互的環節(Connect、Accept、Send、Receive 等)都要作異常處理。
本例中對服務器端 Receive 方法環節作了一些異常處理,並移除了相應的資源,例以下面:
try { length = socketCommunication.Receive(bytes); } catch (SocketException ex) { ShowMsg("出現異常:" + ex.Message); string key = socketCommunication.RemoteEndPoint.ToString(); lbSocketOnline.Items.Remove(key); dictCommunication.Remove(key); dictThread.Remove(key); break; }