淺談C#網(wǎng)絡編程詳解篇
閱讀目錄:
基礎
Socket編程
多線程并發(fā)
阻塞式同步IO
基礎
在現(xiàn)今軟件開發(fā)中,網(wǎng)絡編程是非常重要的一部分,本文簡要介紹下網(wǎng)絡編程的概念和實踐。
Socket是一種網(wǎng)絡編程接口,它是對傳輸層TCP、UDP通信協(xié)議的一層封裝,通過友好的API暴露出去,方便在進程或多臺機器間進行網(wǎng)絡通信。
Socket編程
在網(wǎng)絡編程中分客戶端和服務端兩種角色,比如通過打開瀏覽器訪問到掛在Web軟件上的網(wǎng)頁,從程序角度上來看,即客戶端(瀏覽器)發(fā)起了一個Socket請求到服務器端,服務器把網(wǎng)頁內(nèi)容返回到瀏覽器解析后展示。在客戶端和服務端數(shù)據(jù)通信前,會進行三次確認才會正式建立連接,也即是三次握手。
- 客戶端發(fā)送消息詢問服務端是否準備好
- 服務端回應我準備好了,你呢準備好了嗎
- 客戶端回應服務端我也準備好了,可以通信了
TCP/IP協(xié)議是網(wǎng)絡間通信的基礎協(xié)議,在不同編程語言及不同操作系統(tǒng)下暴露的Socket接口用法也大同小異,僅是其內(nèi)部實現(xiàn)有所不同,比如Linux下的epoll和windows下的IOCP。
服務端
- 實例化Socket
- 把公共地址端口綁定操作系統(tǒng)上
- 開始監(jiān)聽綁定的端口
- 等待客戶端連接
IPEndPoint ip = new IPEndPoint(IPAddress.Any, 6389); Socket listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); listenSocket.Bind(ip); listenSocket.Listen(100); listenSocket.Accept();
listen函數(shù)中有個int類型參數(shù),它表示最大等待處理連接的數(shù)量,表示已建立連接但還未處理的數(shù)量,每調(diào)用Accept函數(shù)一下即從這個等待隊列中拿出一個連接。 通常服務端要服務多個客戶端請求的連接,所以會循環(huán)從等待隊列中拿出連接,進行接收發(fā)送。
while (true) { var accept= listenSocket.Accept(); accept.Receive(); accept.Send(); }
多線程并發(fā)
上面的服務端程序處理接收和發(fā)送消息都是在當前線程下完成的,這意味著要處理完一個客戶端連接后才能去處理下一個連接,如果當前連接是進行數(shù)據(jù)庫或者文件讀取寫入等IO操作,那會極大浪費服務器的CPU資源,降低了服務器吞吐量。
while (true) { var accept = listenSocket.Accept(); ThreadPool.QueueUserWorkItem((obj) => { byte[] receive = new byte[100]; accept.Receive(receive); byte[] send = new byte[100]; accept.Send(receive); }); }
如例子中,當監(jiān)聽到有新連接請求過來時,調(diào)用Accept()取出當前連接的socket,使用新的線程去處理接收和發(fā)送信息,這樣服務端就能實現(xiàn)并發(fā)處理多個客戶端了。 上述代碼中,在高并發(fā)下其實是有問題的,如果客戶端連接請求成千上萬個,那線程數(shù)量也會有這么多,每個線程的棧空間都需要消耗部分內(nèi)存,再加上線程上下文切換,容易導致服務器負載過高,吞吐量大大下降,嚴重時會引起宕機。 當前例子中使用系統(tǒng)ThreadPool的話,線程數(shù)量會固定在一個數(shù)量上,默認是1000,不會無限制開線程,會把處理超出線程數(shù)量的請求放到線程池中的隊列上面。
在unix下類似的實現(xiàn)有2種:
fork一個新進程去處理客戶端的連接:
var connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); var m = fork(); if(m == 0) { //do something }
創(chuàng)建一個新的線程處理限流:
var *clientsockfd = accept(serversockfd,(struct sockaddr *)&clientaddress, (socklent *)&clientlen); if(pthreadcreate(&thread, NULL, recdata, clientsockfd)!=0) { //do something }
阻塞式同步IO
上述例子中使用的即是該模型,使用起來簡單方便。
while (true) { var accept = listenSocket.Accept(); byte[] receive = new byte[100]; accept.Receive(receive); byte[] send = new byte[100]; accept.Send(receive); }
從調(diào)用Receive函數(shù)起到接受到客戶端發(fā)過來的數(shù)據(jù)期間,該函數(shù)會一直阻塞等待著,這個阻塞期間處理流程如下:
- 客戶端發(fā)送數(shù)據(jù)
- 通過廣域網(wǎng)局域網(wǎng)發(fā)送到服務端機器網(wǎng)卡緩沖區(qū)上
- 網(wǎng)卡驅(qū)動對CPU發(fā)送中斷指令
- CPU把數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)
- CPU再把內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝用戶緩沖區(qū),上面的receive字節(jié)數(shù)組。
至此處理成功,開始處理下一個連接請求。 調(diào)用發(fā)送函數(shù)同樣會阻塞在當前,然后把用戶緩沖區(qū)(send字節(jié)數(shù)組)數(shù)據(jù)拷貝到內(nèi)核中TCP發(fā)送緩沖區(qū)中。 TCP的發(fā)送緩沖區(qū)也有一定的大小限制,如果發(fā)送的數(shù)據(jù)大于該限制,send函數(shù)會一直等待發(fā)送緩沖區(qū)有空閑時完全拷貝完才會返回,繼續(xù)處理后續(xù)連接請求。
異步IO
上篇提到用多線程處理多個阻塞同步IO而實現(xiàn)并發(fā)服務端,這種模式在連接數(shù)量比較小的時候非常適合,一旦連接過多,性能會急速下降。 在大多數(shù)服務端網(wǎng)絡軟件中會采用一種異步IO的方式來提高性能。
同步IO方式:連接Receive請求->等待->等待->接收成功
異步IO方式:連接Receive請求->立即返回->事件或回調(diào)通知
采用異步IO方式,意味著單線程可以處理多個請求了,連接發(fā)起一個Receive請求后,當前線程可以立即去做別的事情,當數(shù)據(jù)接收完畢通知線程處理即可。
其數(shù)據(jù)接收分2部分:
數(shù)據(jù)從別的機器發(fā)送內(nèi)核緩沖區(qū)
內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)
第二部分示例代碼:
byte[] msg = new byte[256]; socket.Receive(msg);
介紹這2部分的目的是方便區(qū)分其他幾種方式。 對于用戶程序來說,同步IO和異步IO的區(qū)別在于第二部分是否需要等待。
非阻塞式同步IO
非阻塞式同步IO,由同步IO延伸出來,把這個名詞拆分成2部分描述:
- 非阻塞式,指的是上節(jié)"數(shù)據(jù)從別的機器發(fā)送內(nèi)核緩沖區(qū)"部分是非阻塞的。
- 同步IO,指的是上節(jié)"內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)"部分是等待的。
既然是第一部分是非阻塞的,那就需要一種方法得知什么時候內(nèi)核緩沖區(qū)是OK的。 設置非阻塞模式后,在連接調(diào)用Receive方法時,會立即返回一個標記,告知用戶程序內(nèi)核緩存區(qū)有沒有數(shù)據(jù),如果有數(shù)據(jù)開始進行第二部分操作,從內(nèi)核緩沖區(qū)拷貝到用戶程序緩沖區(qū)。 由于系統(tǒng)會返回個標記,那可以通過輪詢方式來判斷內(nèi)核緩沖區(qū)是否OK。
設置非阻塞模式參考代碼:
SocketInformation sif=new SocketInformation(); sif.Options=SocketInformationOptions.NonBlocking; sif.ProtocolInformation = new byte[24]; Socket socket = new Socket(sif);
輪詢參考代碼:
while(true) { byte[] msg = new byte[256]; var temp = socket.Receive(msg); if (temp=="OK"){ //do something }else{ continue } }
這種方式近乎淘汰了,了解即可。
基于回調(diào)的異步IO
上面介紹過:
異步IO方式:連接Receive請求->立即返回->事件或回調(diào)通知
當回調(diào)到執(zhí)行時,數(shù)據(jù)已經(jīng)在用戶程序緩沖區(qū)已經(jīng)準備好了,在回調(diào)代碼中對這部分數(shù)據(jù)進行相應的邏輯即可。
發(fā)出接收請求:
static byte[] msg = new byte[256]; var temp = socket.BeginReceive(msg, 0, msg.Length, 0, new AsyncCallback(ReadCallback), socket);
回調(diào)函數(shù)中對數(shù)據(jù)做處理:
public static void ReadCallback(IAsyncResult ar) { var socket = (Socket)ar.AsyncState; int read = socket.EndReceive(ar); DoSomething(msg); socket.BeginReceive(msg, 0, msg.Length, 0, new AsyncCallback(Read_Callback), socket); }
當回調(diào)函數(shù)執(zhí)行時,表示數(shù)據(jù)已經(jīng)準備好,需要先結(jié)束接收請求EndReceive,以便第二次發(fā)出接收請求。 在服務端程序中要處理多個客戶端的接收,再次發(fā)出BeginReceive接收數(shù)據(jù)請求即可。
這里的回調(diào)函數(shù)是在另外一個線程的觸發(fā),必要時要對數(shù)據(jù)加鎖防止數(shù)據(jù)競爭:
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
針對C#網(wǎng)絡編程的介紹就到這了,具體的大家可以查看腳本之家之前發(fā)布的文章。
- C# TcpClient網(wǎng)絡編程傳輸文件的示例
- C# 網(wǎng)絡編程之UDP
- c# 網(wǎng)絡編程之tcp
- c# 網(wǎng)絡編程之http
- 深入學習C#網(wǎng)絡編程之HTTP應用編程(下)
- 深入學習C#網(wǎng)絡編程之HTTP應用編程(上)
- 詳解C# 網(wǎng)絡編程系列:實現(xiàn)類似QQ的即時通信程序
- 總結(jié)C#網(wǎng)絡編程中對于Cookie的設定要點
- C# Socket網(wǎng)絡編程實例
- C#網(wǎng)絡編程基礎之進程和線程詳解
- c# socket網(wǎng)絡編程接收發(fā)送數(shù)據(jù)示例代碼
- C#開發(fā)之Socket網(wǎng)絡編程TCP/IP層次模型、端口及報文等探討
- C#網(wǎng)絡編程中常用特性介紹
相關文章
深入理解C#索引器(一種支持參數(shù)的屬性)與屬性的對比
本篇文章是對C#索引器(一種支持參數(shù)的屬性)與屬性的對比進行了詳細的分析介紹,需要的朋友參考下2013-06-06C#使用System.Net.Mail類實現(xiàn)郵件發(fā)送
這篇文章介紹了C#使用System.Net.Mail類實現(xiàn)郵件發(fā)送的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07