【總結】學習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通信。socket

(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的分包,黏包問題,異步編程在後續的文章繼續討論

相關文章
相關標籤/搜索