簡單的Socket圖解,附Python和C#用例代碼蝗岖,以及雙向同時通信示例

寫這篇文章主要是因為自己以前并不怎么用Socket抵赢,在面對Socket時會總想要回避,不明覺厲杰标。但后來仔細(xì)想想其實它很好理解,但是靠一堆術(shù)語講一個概念很容易讓人頭蒙驼仪。所以我想寫篇文章記錄下自己的理解袜漩。另外網(wǎng)上的Socket實例很多都是阻塞式通信(單方向或固定順序宙攻,比如很多聊天小程序)座掘,這里我也針對雙向同時通信寫了一些示例代碼柔滔。

內(nèi)容摘要

  1. Socket簡介
  2. 代碼實例
    1. C#實例
    2. Python實例
    3. 雙向同時通信(Python實例)

內(nèi)容開始

網(wǎng)絡(luò)可以理解為連接睛廊、連好了就傳數(shù)據(jù)杉编,這其中有一對協(xié)議來確定怎么建立連接邓馒、怎么保證數(shù)據(jù)傳輸?shù)耐暾缘裙夂āocket是這樣一個東西,你告訴它改览,你要連誰宝当,然后連接成功了就可以收發(fā)數(shù)據(jù)了胆萧,不用關(guān)心協(xié)議的具體實現(xiàn)跌穗。所以說它是對網(wǎng)絡(luò)通信中一堆協(xié)議的封裝蚌吸,讓你基本不用考慮底層實現(xiàn)就能輕松實現(xiàn)網(wǎng)絡(luò)通信。

TCP/UDP和Socket的關(guān)系

TCP/UDP是真正的通信方式奕枢,Socket是對他們的封裝缝彬。什么意思呢哺眯,

  1. TCP的面向連接和UDP的面向無連接什么意思?
    1. 可以把TCP理解為打電話撼玄,UDP理解為寫信违施。TCP需要先建立連接磕蒲,確保對方在線辣往,才能進(jìn)行通信。而UDP則是你把對方的地址寫好坊萝,發(fā)出去就行了十偶,收到收不到也不用管(基于UDP協(xié)議做優(yōu)化不在本文討論范圍內(nèi))园细。
    2. 使用Socket時的區(qū)別:
      • 發(fā)送信息:TCP必須連接成功后才能發(fā)送(電話接通后你說話才有意義)猛频,UDP直接發(fā)送就好了(寄信知道地址就行了)
      • 接收信息:TCP必須綁定IP和端口號監(jiān)聽連入鹿寻,然后建立連接(接電話)。UDP只要綁定了IP和端口號就行(房子在就能收到信)
  2. TCP的面向流連接和UDP的面向報文
    1. 流坦敌,可以理解為數(shù)據(jù)是源源不斷的到達(dá)恬试。報文,可以理解為數(shù)據(jù)是一次到達(dá)的妇拯。這個東西可以結(jié)合上面的面向連接和面向無連接理解。
    2. 與Socket的關(guān)系
      1. Socket構(gòu)建的時候都需要指定傳輸方式仗嗦。主要就是這兩種稀拐,流傳輸格式(TCP)和數(shù)據(jù)報格式(UDP)德撬。
      2. 傳輸限制蜓洪。單次傳輸限制都存在坯苹,但流的方式可以將大塊數(shù)據(jù)分割粹湃,進(jìn)行多次傳輸为鳄,然后再將內(nèi)容拼接起來,就好像數(shù)據(jù)沒有被分割一樣鉴逞,表現(xiàn)就是使用流傳輸(TCP)的時候我們一般不需要考慮傳輸限制构捡。而數(shù)據(jù)報的話就需要考慮單次傳輸大小限制了勾徽,這個根據(jù)不同的Socket實現(xiàn)也有所不同喘帚。
      3. 傳輸順序咒钟。TCP面向連接和流傳輸?shù)姆绞娇梢员WC數(shù)據(jù)到達(dá)的先后順序朱嘴。而UDP面向無連接和報文傳輸?shù)姆绞綗o法保證數(shù)據(jù)到達(dá)的先后順序,甚至到不到達(dá)都無法保證乌昔。
  3. 常用的其實就是基于TCP的Socket磕道,也就是在構(gòu)建Socket的時候指定使用流傳輸方式溺蕉。而UDP除了需要注意傳輸限制焙贷,使用起來要簡單很多辙芍。下面我們也主要分析TCP這種方式。

基于TCP的Socket的使用流程

Socket這個東西的使用庶灿,在我看來吃衅,很像我們平時打客服電話往踢。比如我們打10086轉(zhuǎn)人工,我們誰都可以打這
個號碼徘层,然后10086會給我們分配一個客服來進(jìn)行真正的交流峻呕。

對各種編程語言來說,也都是一樣的流程趣效。并且網(wǎng)絡(luò)其實可以說是作為一種硬件資源使用的瘦癌,可以看作是對端口的讀寫,所以你只要兩邊的協(xié)議一致(傳輸協(xié)議比如TCP跷敬、傳輸方式比如流、字符編碼比如UTF-8)西傀,理論上就可以正常通信斤寇。它是和語言無關(guān)的,你用Python寫服務(wù)端拥褂,用Java寫客戶端完全沒有問題娘锁,想一下移動端用的推送服務(wù)就是這樣。下面我們就來簡單分析一下饺鹃。

使用流程

  1. 服務(wù)端啟動一個監(jiān)聽用的Socket莫秆,可以稱為listener(listener=10086)
  2. listener不斷的監(jiān)聽有沒有客戶端來連接自己碎税,等待連接,對應(yīng)accept()方法(10086等著客戶撥打這個號碼)
  3. 一旦有可用連接連入(client)馏锡,listener.accept()就會返回一個Socket實例,可以稱為clientExecutor伟端,這個就類似和你交流的客服(你撥打了10086杯道,10086做出反應(yīng),給你分配了一個客服)
  4. 客戶端client和服務(wù)端的clientExecutor可以進(jìn)行通信了(你和客服可以交流了)责蝠,這里需要注意的問題是党巾,兩邊的編碼要一致。

流程圖如下:


Socket使用流程

通信分析

連接建立之后霜医,就可以開始通信了齿拂。其實現(xiàn)可以簡單理解為下面的方式:

  1. 可以認(rèn)為調(diào)用完成send(),數(shù)據(jù)已經(jīng)發(fā)送到對方的緩存中了肴敛。
  2. 調(diào)用receive()從己方緩沖讀數(shù)據(jù)署海。
  3. 關(guān)于同時雙向通信
    1. 如果使用的是TCP的方式,因為TCP是全雙工的医男,可以同時雙向傳輸砸狞。
    2. 如果使用的是UDP的方式,因為UDP是無連接的镀梭,甚至可以同時一對多刀森,多對多傳輸,所以也就沒有相關(guān)的限制报账。
通信示意圖

代碼實例

下面我們寫一些實例代碼研底,并看一下效果。

需求分析

  1. 客戶端在連接成功的時候透罢,會收到服務(wù)端發(fā)送的歡迎消息榜晦。(服務(wù)端發(fā)消息給客戶端)
  2. 然后客戶端可以給服務(wù)端發(fā)送消息。(客戶端發(fā)消息給服務(wù)端)
  3. 服務(wù)端對來自不同客戶端的消息做出反應(yīng)(這里就直接將消息和消息來源打印出來琐凭,實際也可以根據(jù)這些信息做特殊處理)芽隆。

Python實現(xiàn)

服務(wù)端
import socket
import threading
import time

# 當(dāng)新的客戶端連入時會調(diào)用這個方法
def on_new_connection(client_executor, addr):
    print('Accept new connection from %s:%s...' % addr)

    # 發(fā)送一個歡迎信息
    client_executor.send(bytes('Welcome'.encode('utf-8')))

    # 進(jìn)入死循環(huán),讀取客戶端發(fā)送的信息统屈。
    while True:
        msg = client_executor.recv(1024).decode('utf-8')
        if(msg == 'exit'):
            print('%s:%s request close' % addr)
            break
        print('%s:%s: %s' % (addr[0], addr[1], msg))
    client_executor.close()
    print('Connection from %s:%s closed.' % addr)

# 構(gòu)建Socket實例胚吁、設(shè)置端口號和監(jiān)聽隊列大小
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(('192.168.5.103', 9999))
listener.listen(5)
print('Waiting for connect...')

# 進(jìn)入死循環(huán),等待新的客戶端連入愁憔。一旦有客戶端連入腕扶,就分配一個線程去做專門處理。然后自己繼續(xù)等待吨掌。
while True:
    client_executor, addr = listener.accept()
    t = threading.Thread(target=on_new_connection, args=(client_executor, addr))
    t.start()
客戶端
import socket

# 構(gòu)建一個實例半抱,去連接服務(wù)端的監(jiān)聽端口脓恕。
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.5.103', 9999))

# 接收歡迎信息
msg=client.recv(1024)
print('New message from server: %s' % msg.decode('utf-8'))

# 不斷獲取輸入,并發(fā)送給服務(wù)端窿侈。
data=""
while(data!='exit'):
    data=input()
    client.send(data.encode('utf-8'))
client.close()
效果
Python效果

C#實現(xiàn)

服務(wù)端
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerSocket
{
    class Program
    {
        static void Main(string[] args)
        {
            // 構(gòu)建Socket實例炼幔、設(shè)置端口號和監(jiān)聽隊列大小
            var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            string host = "192.168.5.103";
            int port = 9999;
            listener.Bind(new IPEndPoint(IPAddress.Parse(host), port));
            listener.Listen(5);
            Console.WriteLine("Waiting for connect...");

            // 進(jìn)入死循環(huán),等待新的客戶端連入史简。一旦有客戶端連入乃秀,就分配一個Task去做專門處理。然后自己繼續(xù)等待圆兵。
            while(true){
                var clientExecutor=listener.Accept();
                Task.Factory.StartNew(()=>{
                    // 獲取客戶端信息跺讯,C#對(ip+端口號)進(jìn)行了封裝。
                    var remote=clientExecutor.RemoteEndPoint;
                    Console.WriteLine("Accept new connection from {0}",remote);

                    // 發(fā)送一個歡迎消息
                    clientExecutor.Send(Encoding.UTF32.GetBytes("Welcome"));

                    // 進(jìn)入死循環(huán)殉农,讀取客戶端發(fā)送的信息
                    var bytes=new byte[1024];
                    while(true){
                        var count=clientExecutor.Receive(bytes);
                        var msg=Encoding.UTF32.GetString(bytes,0,count);
                        if(msg=="exit"){
                            System.Console.WriteLine("{0} request close",remote);
                            break;
                        }
                        Console.WriteLine("{0}: {1}",remote,msg);
                        Array.Clear(bytes,0,count);
                    }
                    clientExecutor.Close();
                    System.Console.WriteLine("{0} closed",remote);
                });
            }
        }
    }
}
客戶端
using System;
using System.Threading;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ClientSocket
{
    class Program
    {
        static void Main(string[] args)
        {
            var host="192.168.5.103";
            var port=9999;

            // 構(gòu)建一個Socket實例刀脏,并連接指定的服務(wù)端。這里需要使用IPEndPoint類(ip和端口號的封裝)
            Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);

            try
            {
                client.Connect(new IPEndPoint(IPAddress.Parse(host),port));
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return;
            }

            // 接受歡迎信息
            var bytes=new byte[1024];
            var count=client.Receive(bytes);
            Console.WriteLine("New message from server: {0}",Encoding.UTF32.GetString(bytes,0,count));

            // 不斷的獲取輸入超凳,發(fā)送給服務(wù)端
            var input="";
            while(input!="exit"){
                input=Console.ReadLine();
                client.Send(Encoding.UTF32.GetBytes(input));
            }

            client.Close();
        }
    }
}
效果
C#效果

Python與C#互聯(lián)

消息編碼不一致

能發(fā)消息愈污,但是解碼會出現(xiàn)問題。(此處C#方的編碼是UTF32聪建,Python方是UTF-8)

消息編碼不一致
消息編碼一致

消息正常收發(fā)钙畔。

消息編碼一致

雙向自由通信示例(使用Python)

這里旨在驗證是否可以同時收發(fā)信息。

因為不能讓同一個終端即接受輸入又不斷輸出金麸,所以將之前的Python代碼稍作改動擎析,做以下規(guī)定:

  1. 終端只接受輸入,發(fā)送消息挥下。
  2. 收到消息后寫到文件里揍魂。
服務(wù)端
import socket
import threading
import time

# 當(dāng)新的客戶端連入時會調(diào)用這個方法
def on_new_connection(client_executor, addr):
    print('Accept new connection from %s:%s...' % addr)

    # 啟動一個線程進(jìn)入死循環(huán),不斷接收消息棚瘟。
    recy_thread=threading.Thread(target=message_receiver, args=(client_executor,addr))
    recy_thread.start()

    # 不斷獲取輸入现斋,并發(fā)送給服務(wù)端。
    data=""
    while(data!='exit'):
        data=input()
        client_executor.send(data.encode('utf-8'))
    client_executor.close()
    print('Connection from %s:%s closed.' % addr)

# 接收數(shù)據(jù)的線程需要處理的邏輯
def message_receiver(client_executor,addr):
    while True:
        with open('server.txt','a+') as f:
            msg = client_executor.recv(1024).decode('utf-8')
            f.writelines('%s:%s: %s \r\n' % (addr[0], addr[1], msg))

# 構(gòu)建Socket實例偎蘸、設(shè)置端口號和監(jiān)聽隊列大小
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(('192.168.5.103', 9999))
listener.listen(5)
print('Waiting for connect...')

# 進(jìn)入死循環(huán)庄蹋,等待新的客戶端連入。一旦有客戶端連入迷雪,就分配一個線程去做專門處理限书。然后自己繼續(xù)等待。
while True:
    client_executor, addr = listener.accept()
    t = threading.Thread(target=on_new_connection, args=(client_executor, addr))
    t.start()
客戶端
import socket
import threading

# 接收數(shù)據(jù)的線程邏輯
def message_receiver(client):
    while True:
        with open('client.txt','a+') as f:
            msg = client.recv(1024).decode('utf-8')
            f.writelines('%s: %s \r\n' % ('來自服務(wù)端的消息', msg))

# 構(gòu)建一個實例章咧,去連接服務(wù)端的監(jiān)聽端口倦西。
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.5.103', 9999))

# 啟動線程專門用于接收數(shù)據(jù)
recy_thread=threading.Thread(target=message_receiver, args=(client,))
recy_thread.start()

# 不斷獲取輸入,并發(fā)送給服務(wù)端赁严。
data=""
while(data!='exit'):
    data=input()
    client.send(data.encode('utf-8'))
client.close()
效果
Python自由通信
雙向自由通信總結(jié)

其實就是用雙方都用了兩個線程來處理扰柠,一個線程負(fù)責(zé)發(fā)送粉铐,一個線程負(fù)責(zé)接收。Python如此卤档,其他語言也是如此蝙泼。

在真實使用場景中:

  1. 發(fā)送可以是手動調(diào)用而不是等待終端的輸入,接收到數(shù)據(jù)后做些處理而不是簡單的讀到文件中劝枣。
  2. 需要線程同步的地方要注意踱承。
  3. 接收:一般需要使用線程阻塞式接收。發(fā)送:如果不是很頻繁的話哨免,需要發(fā)送的時候異步執(zhí)行一下即可。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昙沦,一起剝皮案震驚了整個濱河市琢唾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盾饮,老刑警劉巖采桃,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異丘损,居然都是意外死亡普办,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門徘钥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衔蹲,“玉大人,你說我怎么就攤上這事呈础∮呤唬” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵而钞,是天一觀的道長沙廉。 經(jīng)常有香客問我,道長臼节,這世上最難降的妖魔是什么撬陵? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮网缝,結(jié)果婚禮上巨税,老公的妹妹穿的比我還像新娘。我一直安慰自己途凫,他們只是感情好垢夹,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著维费,像睡著了一般果元。 火紅的嫁衣襯著肌膚如雪促王。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天而晒,我揣著相機與錄音蝇狼,去河邊找鬼。 笑死倡怎,一個胖子當(dāng)著我的面吹牛迅耘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播监署,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼颤专,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钠乏?” 一聲冷哼從身側(cè)響起栖秕,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晓避,沒想到半個月后簇捍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡俏拱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年暑塑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锅必。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡事格,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搞隐,到底是詐尸還是另有隱情分蓖,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布尔许,位于F島的核電站么鹤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏味廊。R本人自食惡果不足惜蒸甜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望余佛。 院中可真熱鬧柠新,春花似錦、人聲如沸辉巡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至憔恳,卻和暖如春瓤荔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钥组。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工输硝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人程梦。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓点把,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屿附。 傳聞我的和親對象是個殘疾皇子郎逃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353