寫這篇文章主要是因為自己以前并不怎么用Socket抵赢,在面對Socket時會總想要回避,不明覺厲杰标。但后來仔細(xì)想想其實它很好理解,但是靠一堆術(shù)語講一個概念很容易讓人頭蒙驼仪。所以我想寫篇文章記錄下自己的理解袜漩。另外網(wǎng)上的Socket實例很多都是阻塞式通信(單方向或固定順序宙攻,比如很多聊天小程序)座掘,這里我也針對雙向同時通信寫了一些示例代碼柔滔。
內(nèi)容摘要
- Socket簡介
- 代碼實例
- C#實例
- Python實例
- 雙向同時通信(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是對他們的封裝缝彬。什么意思呢哺眯,
- TCP的面向連接和UDP的面向無連接什么意思?
- 可以把TCP理解為打電話撼玄,UDP理解為寫信违施。TCP需要先建立連接磕蒲,確保對方在線辣往,才能進(jìn)行通信。而UDP則是你把對方的地址寫好坊萝,發(fā)出去就行了十偶,收到收不到也不用管(基于UDP協(xié)議做優(yōu)化不在本文討論范圍內(nèi))园细。
- 使用Socket時的區(qū)別:
- 發(fā)送信息:TCP必須連接成功后才能發(fā)送(電話接通后你說話才有意義)猛频,UDP直接發(fā)送就好了(寄信知道地址就行了)
- 接收信息:TCP必須綁定IP和端口號監(jiān)聽連入鹿寻,然后建立連接(接電話)。UDP只要綁定了IP和端口號就行(房子在就能收到信)
- TCP的面向流連接和UDP的面向報文
- 流坦敌,可以理解為數(shù)據(jù)是源源不斷的到達(dá)恬试。報文,可以理解為數(shù)據(jù)是一次到達(dá)的妇拯。這個東西可以結(jié)合上面的面向連接和面向無連接理解。
- 與Socket的關(guān)系
- Socket構(gòu)建的時候都需要指定傳輸方式仗嗦。主要就是這兩種稀拐,流傳輸格式(TCP)和數(shù)據(jù)報格式(UDP)德撬。
- 傳輸限制蜓洪。單次傳輸限制都存在坯苹,但流的方式可以將大塊數(shù)據(jù)分割粹湃,進(jìn)行多次傳輸为鳄,然后再將內(nèi)容拼接起來,就好像數(shù)據(jù)沒有被分割一樣鉴逞,表現(xiàn)就是使用流傳輸(TCP)的時候我們一般不需要考慮傳輸限制构捡。而數(shù)據(jù)報的話就需要考慮單次傳輸大小限制了勾徽,這個根據(jù)不同的Socket實現(xiàn)也有所不同喘帚。
- 傳輸順序咒钟。TCP面向連接和流傳輸?shù)姆绞娇梢员WC數(shù)據(jù)到達(dá)的先后順序朱嘴。而UDP面向無連接和報文傳輸?shù)姆绞綗o法保證數(shù)據(jù)到達(dá)的先后順序,甚至到不到達(dá)都無法保證乌昔。
- 常用的其實就是基于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ù)就是這樣。下面我們就來簡單分析一下饺鹃。
使用流程
- 服務(wù)端啟動一個監(jiān)聽用的Socket莫秆,可以稱為
listener
(listener=10086) -
listener
不斷的監(jiān)聽有沒有客戶端來連接自己碎税,等待連接,對應(yīng)accept()
方法(10086等著客戶撥打這個號碼) - 一旦有可用連接連入(
client
)馏锡,listener.accept()
就會返回一個Socket實例,可以稱為clientExecutor
伟端,這個就類似和你交流的客服(你撥打了10086杯道,10086做出反應(yīng),給你分配了一個客服) - 客戶端
client
和服務(wù)端的clientExecutor
可以進(jìn)行通信了(你和客服可以交流了)责蝠,這里需要注意的問題是党巾,兩邊的編碼要一致。
流程圖如下:
通信分析
連接建立之后霜医,就可以開始通信了齿拂。其實現(xiàn)可以簡單理解為下面的方式:
- 可以認(rèn)為調(diào)用完成
send()
,數(shù)據(jù)已經(jīng)發(fā)送到對方的緩存中了肴敛。 - 調(diào)用
receive()
從己方緩沖讀數(shù)據(jù)署海。 - 關(guān)于同時雙向通信
- 如果使用的是TCP的方式,因為TCP是全雙工的医男,可以同時雙向傳輸砸狞。
- 如果使用的是UDP的方式,因為UDP是無連接的镀梭,甚至可以同時一對多刀森,多對多傳輸,所以也就沒有相關(guān)的限制报账。
代碼實例
下面我們寫一些實例代碼研底,并看一下效果。
需求分析
- 客戶端在連接成功的時候透罢,會收到服務(wù)端發(fā)送的歡迎消息榜晦。(服務(wù)端發(fā)消息給客戶端)
- 然后客戶端可以給服務(wù)端發(fā)送消息。(客戶端發(fā)消息給服務(wù)端)
- 服務(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()
效果
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();
}
}
}
效果
Python與C#互聯(lián)
消息編碼不一致
能發(fā)消息愈污,但是解碼會出現(xiàn)問題。(此處C#方的編碼是UTF32聪建,Python方是UTF-8)
消息編碼一致
消息正常收發(fā)钙畔。
雙向自由通信示例(使用Python)
這里旨在驗證是否可以同時收發(fā)信息。
因為不能讓同一個終端即接受輸入又不斷輸出金麸,所以將之前的Python代碼稍作改動擎析,做以下規(guī)定:
- 終端只接受輸入,發(fā)送消息挥下。
- 收到消息后寫到文件里揍魂。
服務(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()
效果
雙向自由通信總結(jié)
其實就是用雙方都用了兩個線程來處理扰柠,一個線程負(fù)責(zé)發(fā)送粉铐,一個線程負(fù)責(zé)接收。Python如此卤档,其他語言也是如此蝙泼。
在真實使用場景中:
- 發(fā)送可以是手動調(diào)用而不是等待終端的輸入,接收到數(shù)據(jù)后做些處理而不是簡單的讀到文件中劝枣。
- 需要線程同步的地方要注意踱承。
- 接收:一般需要使用線程阻塞式接收。發(fā)送:如果不是很頻繁的話哨免,需要發(fā)送的時候異步執(zhí)行一下即可。