首頁  >  文章  >  後端開發  >  C#網路程式設計的圖文代碼詳解

C#網路程式設計的圖文代碼詳解

黄舟
黄舟原創
2017-03-28 11:20:201406瀏覽

在現今軟體開發中,網頁程式設計是非常重要的一部分,本文簡單介紹下網路程式設計的概念與實作,需要的朋友可以參考下

閱讀目錄:

基礎
Socket程式設計
多執行緒並發
阻塞式同步IO

基礎
在現今軟體開發中,網路程式設計是非常重要的一部分,本文簡要介紹下網路程式設計的概念和實踐。
Socket是一種網路編程接口,它是對傳輸層TCP、UDP通訊協定的一層封裝,透過友善的API暴露出去,方便在進程或多台機器間進行網路通訊。

Socket程式設計

在網路程式設計中分客戶端和服務端兩個角色,例如透過開啟瀏覽器存取到掛在Web軟體上的網頁,從程式角度來看,即客戶端(瀏覽器)發起了一個Socket請求到伺服器端,伺服器把網頁內容返回到瀏覽器解析後展示。在客戶端和服務端資料通訊前,會進行三次確認才會正式建立連接,也即是三次握手。

  1. 客戶端發送訊息詢問服務端是否準備好

  2. 服務端回應我準備好了,你呢準備好了嗎

  3. 客戶端回應服務端我也準備好了,可以通訊了

##TCP/IP協定是網路間通訊的基礎協議,在不同程式語言及不同作業系統下暴露的Socket介面用法也大同小異,僅是其內部實作有所不同,例如Linux下的epoll和windows下的IOCP。

服務端
  • 實例化Socket

  • #把公用位址連接埠綁定作業系統上

  • 開始監聽綁定的連接埠

  • 等待客戶端連線

  • 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

函數中有個int型別參數,它表示最大等待處理連接的數量,表示已建立連接但還未處理的數量,每調用Accept函數一下即從這個等待隊列中拿出一個連接。 通常服務端要服務多個客戶端請求的連接,所以會循環從等待佇列中拿出連接,進行接收發送。

while (true) 
      { 
        var accept= listenSocket.Accept();
        accept.Receive(); 
        accept.Send(); 
      }

多執行緒並發上面的服務端程式處理接收和發送訊息都是在目前執行緒下完成的,這意味著要處理完一個客戶端連接後才能去處理下一個連接,如果當前連接是進行資料庫或檔案讀取寫入等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);
        });
      }

如範例中,當監聽到有新連線請求過來時,呼叫Accept()取出目前連線的socket,使用新的執行緒去處理接收和傳送訊息,這樣服務端就能實現

並發處理多個客戶端了。 上述程式碼中,在高並發下其實是有問題的,如果客戶端連接請求成千上萬個,那麼線程數量也會有這麼多,每個線程的棧空間都需要消耗部分內存,再加上線程上下文切換,容易導致伺服器負載過高,吞吐量大幅下降,嚴重時會造成宕機。 目前範例中使用系統ThreadPool的話,線程數量會固定在一個數量上,預設是1000,不會無限制開線程,會把處理超出線程數量的請求放到線程池中的佇列上面。 在unix下類似的實作有2種:

fork一個新程序去處理客戶端的連線:

var connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); 
var m = fork(); 
if(m == 0) 
{
 //do something 
}

建立一個新的執行緒處理限流:

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);
      }

從呼叫Receive函數起到接受到客戶端發過來的資料期間,該函數會一直阻塞等待著,這個阻塞期間處理流程如下:

  1. 用戶端傳送資料

  2. 透過廣域網路區域網路傳送到服務端機器網路卡緩衝區上

  3. 網路卡驅動對CPU傳送中斷指令

  4. CPU把資料拷貝到核心緩衝區

  5. CPU再把核心緩衝區的資料拷貝使用者緩衝區,上面的receive位元組數組。

至此处理成功,开始处理下一个连接请求。 调用发送函数同样会阻塞在当前,然后把用户缓冲区(send字节数组)数据拷贝到内核中TCP发送缓冲区中。 TCP的发送缓冲区也有一定的大小限制,如果发送的数据大于该限制,send函数会一直等待发送缓冲区有空闲时完全拷贝完才会返回,继续处理后续连接请求。

异步IO
上篇提到用多线程处理多个阻塞同步IO而实现并发服务端,这种模式在连接数量比较小的时候非常适合,一旦连接过多,性能会急速下降。 在大多数服务端网络软件中会采用一种异步IO的方式来提高性能。

同步IO方式:连接Receive请求->等待->等待->接收成功
异步IO方式:连接Receive请求->立即返回->事件或回调通知
采用异步IO方式,意味着单线程可以处理多个请求了,连接发起一个Receive请求后,当前线程可以立即去做别的事情,当数据接收完毕通知线程处理即可。
其数据接收分2部分:

数据从别的机器发送内核缓冲区
内核缓冲区拷贝到用户缓冲区
第二部分示例代码:

byte[] msg = new byte[256]; socket.Receive(msg);

介绍这2部分的目的是方便区分其他几种方式。 对于用户程序来说,同步IO和异步IO的区别在于第二部分是否需要等待。

非阻塞式同步IO
非阻塞式同步IO,由同步IO延伸出来,把这个名词拆分成2部分描述:

  • 非阻塞式,指的是上节"数据从别的机器发送内核缓冲区"部分是非阻塞的。

  • 同步IO,指的是上节"内核缓冲区拷贝到用户缓冲区"部分是等待的。

既然是第一部分是非阻塞的,那就需要一种方法得知什么时候内核缓冲区是OK的。 设置非阻塞模式后,在连接调用Receive方法时,会立即返回一个标记,告知用户程序内核缓存区有没有数据,如果有数据开始进行第二部分操作,从内核缓冲区拷贝到用户程序缓冲区。 由于系统会返回个标记,那可以通过轮询方式来判断内核缓冲区是否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 }
}

这种方式近乎淘汰了,了解即可。

基于回调的异步IO
上面介绍过:

异步IO方式:连接Receive请求->立即返回->事件或回调通知
当回调到执行时,数据已经在用户程序缓冲区已经准备好了,在回调代码中对这部分数据进行相应的逻辑即可。

发出接收请求:

static byte[] msg = new byte[256]; 
var temp = socket.BeginReceive(msg, 0, msg.Length, 0, new AsyncCallback(ReadCallback), socket);

回调函数中对数据做处理:

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);
}

当回调函数执行时,表示数据已经准备好,需要先结束接收请求EndReceive,以便第二次发出接收请求。 在服务端程序中要处理多个客户端的接收,再次发出BeginReceive接收数据请求即可。

这里的回调函数是在另外一个线程的触发,必要时要对数据加锁防止数据竞争:

Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

以上是C#網路程式設計的圖文代碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn