【總結】學習Socket編寫的聊天室小程序


1.前言

在學習Socket之前,先來學習點網絡相關的知識吧,自己學習過程中的一些總結,Socket是一門很高深的學問,本文只是Socket一些最基礎的東西大神請自覺繞路。

傳輸協議

TCP:Transmission Control Protocol 傳輸控制協議TCP是一種面向連接(連接導向)的、可靠的、基於字節流的運輸層(Transport layer)通信協議。
特點:
面向連接的協議,數據傳輸必須要建立連接,所以在TCP中需要連接時間。
傳輸數據大小限制,一旦連接建立,雙方可以按統一的格式傳輸大的數據。
一個可靠的協議,確保接收方完全正確地獲取發送方所發送的全部數據。
說到TCP就不得不說經典的三次握手。
在TCP/IP協議中,TCP協議通過三次握手建立一個可靠的連接

第一次握手:客戶端嘗試連接服務器,向服務器發送syn包(同步序列編號Synchronize Sequence Numbers),syn=j,客戶端進入SYN_SEND狀態等待服務器確認

第二次握手:服務器接收客戶端syn包並確認(ack=j+1),同時向客戶端發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態

第三次握手:第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手

 
UDP: User Datagram Protocol的簡稱, 中文名是用戶數據包協議,是 OSI 參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。
特點:
每個數據報中都給出了完整的地址信息,因此無需要建立發送方和接收方的連接。
UDP傳輸數據時是有大小限制的,每個被傳輸的數據報必須限定在64KB之內。
UDP是一個不可靠的協議,發送方所發送的數據報並不一定以相同的次序到達接收方。
 
 

TCP協議:就好比兩個電話機 通過電話線進行數據交互的格式約定

HTTP協議:就好比兩個人 通過電話機 說話的語法。

(1)公認端口(WellKnownPorts):從0到1023,它們緊密綁定(binding)於一些服務。通常這些端口的通訊明確表明了某種服務的協議。例如:80端口實際上總是HTTP通訊。

(2)注冊端口(RegisteredPorts):從1024到49151。它們松散地綁定於一些服務。也就是說有許多服務綁定於這些端口,這些端口同樣用於許多其它目的。例如:許多系統處理動態端口從1024左右開始。

(3)動態和/或私有端口(Dynamicand/orPrivatePorts):從49152到65535。理論上,不應為服務分配這些端口。實際上,機器通常從1024起分配動態端口。

 

OSI網絡7層模型

TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標准的協議集,它是為廣域網(WANs)設計的。
UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。
應用層 (Application):應用層是個很廣泛的概念,有一些基本相同的系統級 TCP/IP 應用以及應用協議,也有許多的企業商業應用和互聯網應用。
傳輸層 (Transport):傳輸層包括 UDP 和 TCP,UDP 幾乎不對報文進行檢查,而 TCP 提供傳輸保證。
網絡層 (Network):網絡層協議由一系列協議組成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。
鏈路層 (Link):又稱為物理數據網絡接口層,負責報文傳輸。
 
IP地址
每台聯網的電腦都有一個唯一的IP地址。
長度32位,分為四段,每段8位,用十進制數字表示,每段范圍 0 ~ 255
特殊IP:127.0.0.1 用戶本地網卡測試
版本:V4(32位) 和 V6(128位,分為8段,每段16位)
 
端口
在網絡上有很多電腦,這些電腦一般運行了多個網絡程序。每種網絡程序都打開一個Socket,並綁定到一個端口上,不同的端口對應於不同的網絡程序。
常用端口:21 FTP  ,25 SMTP  ,110 POP3  ,80 HTTP , 443 HTTPS
 
有兩種常用Socket類型:
流式Socket(STREAM):
是一種面向連接的Socket,針對於面向連接的TCP服務應用,安全,但是效率低
 
數據報式Socket(DATAGRAM):
是一種無連接的Socket,對應於無連接的UDP服務應用.不安全(丟失,順序混亂,在接收端要分析重排及要求重發),但效率高.
 
說了那么多,讓我們來看看socket在網絡7層協議中的位置。如下圖所示

2.聊天室原理

 Socket 流式(服務器端和客戶端
服務器端的Socket(至少需要兩個)
一個負責接收客戶端連接請求(但不負責與客戶端通信)
每成功接收到一個客戶端的連接便在服務端產生一個對應的負責通信的Socket
在接收到客戶端連接時創建.
為每個連接成功的客戶端請求在服務端都創建一個對應的Socket(負責和客戶端通信).
 
客戶端的Socket
客戶端Socket
必須指定要連接的服務端IP地址和端口。
通過創建一個Socket對象來初始化一個到服務器端的TCP連接
 
 Socket的通訊過程
服務器端:
申請一個socket
綁定到一個IP地址和一個端口上
開啟偵聽,等待接授連接
 
客戶端:
申請一個socket
連接服務器(指明IP地址和端口號)
l服務器端接到連接請求后,產生一個新的socket(端口大於1024)與客戶端建立連接並進行通訊,原監聽socket繼續監聽。
 
Socket常用的一些類和方法
IPAddress類:包含了一個IP地址
IPEndPoint類:包含了一對IP地址和端口號
Socket (): 創建一個Socket
Bind(): 綁定一個本地的IP和端口號(IPEndPoint)
Listen(): 讓Socket偵聽傳入的連接嘗試,並指定偵聽隊列容量
Connect(): 初始化與另一個Socket的連接
Accept(): 接收連接並返回一個新的socket
Send(): 輸出數據到Socket
Receive(): 從Socket中讀取數據
Close(): 關閉Socket (銷毀連接)
 

3.聊天室代碼

服務器端代碼:

using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Server
{
    using System.Net.Sockets;
    using System.Net;
    using System.Threading;
    public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        } 
 
         
        //服務端 監聽套接字
        Socket socketWatch = null;
        //服務端 監聽線程
        Thread threadWatch = null;
        //字典集合:保存 通信套接字
        Dictionary<string, Socket> dictCon = new Dictionary<string, Socket>(); 
        private void btnStartListen_Click(object sender, EventArgs e)
        {

            try
            {
                //1.創建監聽套接字 使用 ip4協議,流式傳輸,TCP連接
                socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                //2.綁定端口
                //2.1獲取網絡節點對象
                IPAddress address = IPAddress.Parse(txtIP.Text);
                IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text));
                //2.2綁定端口(其實內部 就向系統的 端口表中 注冊 了一個端口,並指定了當前程序句柄)
                socketWatch.Bind(endPoint);
                //2.3設置監聽隊列
                socketWatch.Listen(10);
                //2.4開始監聽,調用監聽線程 執行 監聽套接字的 監聽方法
                threadWatch = new Thread(WatchConnecting);
                threadWatch.IsBackground = true;
                threadWatch.Start();
                ShowMsg("楓伶憶,服務器啟動啦!");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                throw;
            }


        } 
        void WatchConnecting()
        {
            //2.4開始監聽:此方法會阻斷當前線程,直到有 其它程序 連接過來,才執行完畢
            Socket sokMsg = socketWatch.Accept();
            //將當前連接成功的 【與客戶端通信的套接字】 的 標識 保存起來,並顯示到 列表中
            //將 遠程客戶端的 ip和端口 字符串 存入 列表
            this.lbOnline.Items.Add(sokMsg.RemoteEndPoint.ToString());
            //將 服務端的通信套接字 存入 字典集合
            dictCon.Add(sokMsg.RemoteEndPoint.ToString(), sokMsg);

            ShowMsg("有客戶端連接了!");
            //2.5創建 通信線程
            Thread thrMsg = new Thread(ReceiveMsg);
            thrMsg.IsBackground = true;
            thrMsg.Start(sokMsg);
        }
        void ReceiveMsg(object obj)
        {
            try
            {
                Socket sokMsg = obj as Socket;
                //3.通信套接字 監聽 客戶端的 消息
                //3.1創建 消息緩存區
                byte[] arrMsg = new byte[1024 * 1024 * 1];
                while (isReceive)
                {
                    //3.2接收客戶端的消息 並存入 緩存區,注意:Receive方法也會阻斷當前的線程
                    sokMsg.Receive(arrMsg);
                    //3.3將接收到的消息 轉成 字符串
                    string strMsg = System.Text.Encoding.UTF8.GetString(arrMsg);
                    //3.4將消息 顯示到 文本框
                    ShowMsg(strMsg);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                throw;
            }
        }
        void ShowMsg(string strmsg)
        {
            this.txtShow.AppendText(strmsg + "\r\n");
        } 
        private void btnSend_Click_1(object sender, EventArgs e)
        {

            string strClient = this.lbOnline.Text;
            if (string.IsNullOrEmpty(strClient))
            {
                MessageBox.Show("請選擇你要發送消息的客戶端");
                return;
            }
            if (dictCon.ContainsKey(strClient))
            {
                string strMsg = this.txtInput.Text.Trim();
                ShowMsg("\r\n向客戶端【" + strClient + "】說:" + strMsg);

                //使用 指定的 通信套接字 將 字符串 發送到 指定的客戶端
                byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg);
                dictCon[strClient].Send(arrMsg);
            }
            this.txtInput.Text = "";
        }
     }

}

 客戶端代碼:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Client
{
    using System.Net.Sockets;
    using System.Net;
    using System.Threading;
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        } 
 
         
       //客戶端 通信套接字
        Socket socketMsg = null;
        //客戶端 通信線程
        Thread threadMsg = null;

        bool isRec = true;//標記任務
        private void btnConnect_Click(object sender, EventArgs e)
        {
            try
            {
                //1.創建監聽套接字 使用 ip4協議,流式傳輸,TCP連接
                socketMsg = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                //2.獲取要連接的服務端 節點
                //2.1獲取網絡節點對象
                IPAddress address = IPAddress.Parse(txtIP.Text);
                IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text));
                //3.向服務端 發送鏈接請求
                socketMsg.Connect(endPoint);
                ShowMsg("連接服務器成功~~!");
                //4.開啟通信線程
                threadMsg = new Thread(RecevieMsg);
                threadMsg.IsBackground = true;
                threadMsg.Start();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                throw;
            }

        } 
        void RecevieMsg()
        {
            try
            {
                //3.1創建 消息緩存區
                byte[] arrMsg = new byte[1024 * 1024 * 1];
                while (isRec)
                {
                    socketMsg.Receive(arrMsg);
                    string strMsg = System.Text.Encoding.UTF8.GetString(arrMsg);
                    ShowMsg("\r\n服務器說:" + strMsg);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                throw;
            }
        } 
        private void btnSend_Click_1(object sender, EventArgs e)
        {
            string strMsg = this.txtInput.Text.Trim();
            byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg);
            socketMsg.Send(arrMsg);
            this.txtInput.Text = "";
        }
        void ShowMsg(string strmsg)
        {
            this.txtShow.AppendText(strmsg + "\r\n");
        } 

}

}

 最終的效果圖如下:

 

4.注意

至少要定義一個要連接的遠程主機的IP和端口號。

端口號必須在 1 和 65535之間,最好在1024以后。
要連接的遠程主機必須正在監聽指定端口,也就是說你無法隨意連接遠程主機。
如:
IPAddress addr = IPAddress.Parse("127.0.0.1");
IPEndPoint endp = new IPEndPoint(addr, 8989);

  服務端先綁定:serverWelcomeSocket.Bind(endp)

  客戶端再連接:clientSocket.Connect(endp)

 
一個Socket一次只能連接一台主機。
Socket關閉后無法再次使用。
每個Socket對象只能一台遠程主機連接. 如果你想連接到多台遠程主機, 你必須創建多個Socket對象

 5.擴展

l實現傳送文件
如果接收數據是文件還是文字?
設計"協議":
把要傳遞的字節數組前面都加上一個字節做為標識。0:表示文字  1:表示文件
即:文字:  0+文字(字節數組表示)
文件:1+文件的二進制信息
 

 比如Socket的分包,黏包問題,異步編程在后續的文章繼續討論

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM