同步傳輸字串
同步傳輸字串
接下來考慮著一種情況,完成一個簡單的文字通訊:
(1).客戶端將字串傳送到服務端,服務端接受字串並顯示
(2).服務端將字串由英文的小寫轉換為大寫,然後發回給客戶端,客戶端接受並顯示.
客戶端傳送,服務端接受並輸出
1.服務端程式
可以在TcpClient上呼叫GetStream()方法來獲得連線到遠端計算機的網路流NetworkStream.當在客戶端呼叫時,它獲得連線服務端的流;當在服務端呼叫時,它獲得連線客戶端的流.
先看服務端的程式碼實現:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
namespace Server
{
class Program
{
static void Main(string[] args)
{
const int BufferSize = 8192;//快取大小,8192位元組
Console.WriteLine("Server is running...");
IPAddress ip = new IPAddress(new byte[] { 192, 168, 3, 19 });
TcpListener listener = new TcpListener(ip,1621);
listener.Start();//開始監聽
Console.WriteLine("Start Listening...");
//獲取一個連線,中斷方法
TcpClient remoteClient = listener.AcceptTcpClient();
//列印連線到的客戶端資訊
Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流,並寫入buffer中
NetworkStream streamToClient = remoteClient.GetStream();
byte[] buffer = new byte[BufferSize];
int byteRead = streamToClient.Read(buffer,0,BufferSize);
//獲得請求的字串
string msg = Encoding.Unicode.GetString(buffer,0,byteRead);
Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);
//按Q退出
Console.WriteLine("\n\n按Q退出\n\n");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key!=ConsoleKey.Q);
}
}
}
等一下,這裡樓主有個問題需要問一下,在使用netstat命令檢視埠時,你知道根據埠的狀態判斷哪些埠是可以用來監聽的,那些埠不能用來監聽嗎?
這段程式的上半部分上一次說過了,remoteClient.GetStream()方法獲取到了連線至客戶端的流,然後從流中讀出資料並儲存在了buffer中,隨後使用Encoding.Unicode.GetString()方法,從快取中獲取到實際的字串.最後將字串顯示在了控制檯中.這段程式碼有個地方需要注意:如果能夠讀取的字串的總位元組數大於BufferSize,就會出現字串截斷現象,只能讀取到不完整的字串.這是因為快取的位元組數是有限的,在本例中是8192.如果傳遞的資料位元組數比較大,例如圖片,音訊,檔案,則必須採用”分次讀取然後轉存”的方式,程式碼如下:
//獲取字串
byte buffer = new byte[BufferSize];
int bytesRead;
MemoryStream ms = new MemoryStream();
do
{
bytesRead = streamToClient.Read(buffer,0,BufferSize);
} while (bytesRead>0);
我們們沒有使用”分次讀取轉存”的方式,為啥呢?因為:樓主的水平有限,不想誤人子弟.
說實話,8192已經很多了.當使用Unicode編碼時,8192位元組可以儲存4096個漢字和英文字元.使用不同的編碼方式,佔用的位元組數有很大的差異.
現在不對客戶端在任何修改,先除錯執行伺服器,在執行客戶端,會發現服務端在列印完”Client Connected ! Local:192.168.3.19:1621<-- Client:192.168.3.19:4044”之後,再次被阻塞了,沒有繼續執行,也沒有輸出”Reading data,{0} bytes...”等任何字元.可見,與AcceptTcpClient()方法類似,Read()方法也是同步的,只有當客戶端傳送資料的時候,服務端才會讀取資料,執行此方法,否則會一直等待下去.
2.客戶端程式
接下來編寫客戶端想服務端傳送字串的程式碼,與服務端類似,先獲取連線到服務端的流,將字串儲存到buffer中,再將快取寫入流.寫入流這一過程,相當於將訊息發往服務端.
static void Main(string[] args)
{
#region MyRegion
/*
Console.WriteLine("Client is running...");
TcpClient client;
for (int i = 0; i <= 2; i++)
{
try
{
client = new TcpClient();
//與伺服器建立連線
client.Connect(IPAddress.Parse("192.168.3.19"), 1621);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
//列印連線到的服務端資訊
Console.WriteLine("Server Connected ! Local:{0} -->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
}
//按Q退出
Console.WriteLine("\n\n輸入\"Q\"鍵退出. ");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key != ConsoleKey.Q);*/
#endregion
Console.WriteLine("Client is running...");
TcpClient client;
try
{
client = new TcpClient();
//與伺服器建立連線
client.Connect(IPAddress.Parse("192.168.1.120"),1621);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
//列印連線到的服務端資訊
Console.WriteLine("Server Connected! Local:{0}-->Server:{1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
string msg = "Hello,readers!";
NetworkStream streamToServer = client.GetStream();
byte[] buffer = Encoding.Unicode.GetBytes(msg);//獲得快取
streamToServer.Write(buffer,0,buffer.Length);
Console.WriteLine("Sent: {0}",msg);
//按Q退出
Console.WriteLine("\n\n按Q退出");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key!=ConsoleKey.Q);
}
現在再次執行程式,得到的輸出為:
//服務端:
Server is running...
Start Listening...
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:5465
Received: Hello,readers! [28bytes]
//客戶端:
Client is running...
Server Connected! Local:192.168.1.120:5465-->Server:192.168.1.120:1621
Sent: Hello,readers!
可以看到,已經成功的傳送和接受了一串字串,是不是有點成就感了?QQ不過如此了了的事.但不要高興的太早,對於即時通訊程式來說,在客戶端和服務端之間,應該是可以不間斷的接受和傳送訊息的.但是上面的程式碼只能接受客戶端傳送的一條訊息,因為程式碼已經執行完畢,控制外也已經輸出了”按Q退出”.無法再繼續執行任何的後續動作.此時如果在開啟一個客戶端,那麼出現的情況是:客戶端可以與伺服器建立連線,也就是”netstat -a”顯示為ESTABLISHED,這是作業系統所知道的;但是由於服務端的程式已經執行到了最後一步,只能輸入Q退出,無法再執行任何動作.
回想一下,前面說過,當一個服務端需要接受多個客戶端連線時,所採用的處理辦法是:將AcceptTcpClient()方法放在一個while迴圈中.類似的,當服務端需要同一個客戶端的多次請求進行處理時,可以將Read()方法也放入到一個do/while迴圈中.
綜合起來就只有四種情況:
第一種:如果不使用do/while迴圈,服務端只有一個listener.AcceptTcpClient()方法和一個TcpClient.GetStream().Read()方法,則服務端只能處理來自一個客戶端的一條請求.
第二種:如果使用一個do/while迴圈,並將listener.AcceptTcpClient(0方法和TcpClient.GetStream().Read()方法放在迴圈中,那麼服務端將可以處理多個客戶端的一條請求.
第三種:使用一個do/while迴圈,並將listener.AcceptTcpClient()方法放在迴圈內,將TcpClient.GetStream().Read()方法放在迴圈外,那麼可以處理一個客戶端的多條請求.
第四種:使用兩個do/while迴圈,對它們分別進行巢狀,那麼結果是啥?你肯定會說,可以處理多個客戶端的多個請求.事實上不是這樣的.因為內層的do/while迴圈總是在為一個客戶端服務,它會中斷在TcpClient.GetStream().Read()方法的位置,而無法執行完畢.即時可以通過某種方式讓內層迴圈退出,例如,當客戶端向服務端傳送”exit”字串時,服務端也只能挨個對客戶端提供服務.如果服務端想並行的對多個客戶端的多個請求進行服務,那麼服務端就需要採用多執行緒.主執行緒,即執行外層的do/while迴圈的執行緒,它在AcceptTcpClient()獲取到一個TcpClient之後,必須將內層的do/while迴圈交給其他的執行緒去處理,然後主執行緒快速的重新回到listener.AcceptTcpClient()的位置,來響應其他的客戶端?明白了嗎?是不是有點暈,樓主也有點暈,沒關係.我們們一個一個講解.
我們們先來看第二種和第三種情況.
對於第二種情況,按照上面描述的那些對程式碼做一些改動,服務端程式碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.IO;
namespace Server
{
class Program
{
static void Main(string[] args)
{
const int BufferSize = 8192;//快取大小,8192位元組
Console.WriteLine("Server is running...");
IPAddress ip = new IPAddress(new byte[] { 192, 168, 1, 120 });
TcpListener listener = new TcpListener(ip,1621);
listener.Start();//開始監聽
Console.WriteLine("Start Listening...");
do
{
//獲取一個連線,中斷方法
TcpClient remoteClient = listener.AcceptTcpClient();
//列印連線到的客戶端資訊
Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
//獲得流,並寫入buffer中
NetworkStream streamToClient = remoteClient.GetStream();
byte[] buffer = new byte[BufferSize];
int bytesRead = streamToClient.Read(buffer,0,BufferSize);
//獲得請求的字串
string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);
} while (true);
/*
//獲取一個連線,中斷方法
TcpClient remoteClient = listener.AcceptTcpClient();
//列印連線到的客戶端資訊
Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流,並寫入buffer中
NetworkStream streamToClient = remoteClient.GetStream();
byte[] buffer = new byte[BufferSize];
int byteRead = streamToClient.Read(buffer,0,BufferSize);
//獲得請求的字串
string msg = Encoding.Unicode.GetString(buffer,0,byteRead);
Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);
//按Q退出
Console.WriteLine("\n\n按Q退出\n\n");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key!=ConsoleKey.Q);
*/
/* //獲取字串
byte buffer = new byte[BufferSize];
int bytesRead;
MemoryStream ms = new MemoryStream();
do
{
bytesRead = streamToClient.Read(buffer,0,BufferSize);
} while (bytesRead>0);*/
}
}
}
然後啟動多個客戶端程式,在服務端可以看到這樣的情況:
Server is running...
Start Listening...
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6737
Received: Hello,readers! [28bytes]
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6742
Received: Hello,readers! [28bytes]
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6754
Received: Hello,readers! [28bytes]
現在將第二種情況變為第三種情況,只需要將do向下挪動幾行就可以了:
//獲取一個連線,中斷方法
TcpClient remoteClient = listener.AcceptTcpClient();
//列印連線到的客戶端資訊
Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
//獲得流,並寫入buffer中
NetworkStream streamToClient = remoteClient.GetStream();
do
{
byte[] buffer = new byte[BufferSize];
int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
//獲得請求的字串
string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);
} while (true);
然後再改動一下客戶端,讓它可以連續傳送多個字串到到伺服器.當按下回車的時候傳送字串,輸入”Q”的時候,跳出迴圈:
Console.WriteLine("Client is running...");
TcpClient client;
try
{
client = new TcpClient();
//與伺服器建立連線
client.Connect(IPAddress.Parse("192.168.1.120"), 1621);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
//列印連線到的服務端資訊
Console.WriteLine("Server Connected! Local:{0}-->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
NetworkStream streamToServer = client.GetStream();
string msg;
do
{
Console.Write("Sent:");
msg = Console.ReadLine();
if (!String.IsNullOrEmpty(msg) && msg != "Q")
{
byte[] buffer = Encoding.Unicode.GetBytes(msg);
try
{
//發往伺服器
streamToServer.Write(buffer, 0, buffer.Length);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
}
} while (msg != "Q");
接下來先執行服務端,再執行客戶端,輸入以下字串來進行測試,應該能夠看到預期的結果.
注意:如果再開啟一個客戶端,該客戶端雖然可以成功的連線服務端,也可以傳送字串,但是服務端不會做任何的處理和顯示.
這裡有一點需要注意:當客戶端在TcpClient例項上呼叫Close()方法,或者在流上呼叫Didpose()方法時,服務端的streamToClient.Read()方法會持續返回0,但是不丟擲異常,所以會產生一個無限迴圈.服務端不斷的重新整理顯示”Received: [0 bytes]”,因此,服務端在呼叫streamToClient.Read()方法後,應加上一個如下判斷:
int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
if (bytesRead==0)
{
Console.WriteLine("Client offline");
break;
}
如果直接關閉掉客戶端,或者客戶端執行完畢但沒有呼叫stream.Dispose()或者TcpClient.Close(),切服務端此時仍阻塞在Read()方法處,則會在服務端丟擲異常:未經處理的異常: System.IO.IOException: 無法從傳輸連線中讀取資料:遠端主機強迫關閉了一個現有的連線。
因此,服務端的streamToClient.Read()方法需要寫在一個try/catch中.下面是改進的服務端程式碼:
do
{
try
{
byte[] buffer = new byte[BufferSize];
int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
if (bytesRead == 0)
{
Console.WriteLine("Client offline");
break;
}
//獲得請求的字串
string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
break;
}
} while (true);
同樣的,如果服務端在連線到客戶端之後呼叫remoteClient.Close(),則客戶端在呼叫streamToServer.Write()時也會丟擲異常.因此,他們的讀寫操作都必須放入try/catch塊中.
服務端傳送,客戶端接受並顯示
1.服務端程式
到現在為止,客戶端已經能傳送字串到服務端,服務端能接受並顯示.接下來,再進行進一步處理,使服務端將字串有英文小寫改為英文大寫,然後發回給客戶端,客戶端接受並顯示.此時服務端和客戶端的角色和上面進行了一下對調:對於服務端來說,就好像剛才的客戶端一樣,將字串寫入到流中,而客戶端則同服務端一樣,接受並顯示.
服務端:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.IO;
namespace Server
{
class Program
{
static void Main(string[] args)
{
const int BufferSize = 8192;//快取大小,8192位元組
Console.WriteLine("Server is running...");
IPAddress ip = new IPAddress(new byte[] { 192,168,1,120});
TcpListener listener = new TcpListener(ip, 1621);
//開始監聽
listener.Start();
Console.WriteLine("Start Listening...");
//獲取一個連線,中斷方法
TcpClient remoteClient = listener.AcceptTcpClient();
//列印連線到的客戶端資訊
Console.WriteLine("Client Connected! Local:{0}<--Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流
NetworkStream streamToClient = remoteClient.GetStream();
do
{
try
{
byte[] buffer = new byte[BufferSize];
int bytesRead = streamToClient.Read(buffer,0,BufferSize);
if (bytesRead==0)
{
Console.WriteLine("Client offline");
break;
}
//獲得請求的字串
string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine("Received: {0} [{1} bytes]",msg,bytesRead);
//轉換為大寫
msg = msg.ToUpper();
buffer = Encoding.Unicode.GetBytes(msg);
streamToClient.Write(buffer,0,buffer.Length);
Console.WriteLine("Sent: {0}",msg);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
break;
}
} while (true);
streamToClient.Dispose();
remoteClient.Close();
//按Q退出
Console.WriteLine("\n\n按Q退出");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key!=ConsoleKey.Q);
}
}
}
上面的程式碼大家應該很熟悉了,主要的變化是轉換字母大小寫,並寫入到流中的操作.
2.客戶端程式碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Client
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("CLient is running...");
TcpClient client;
const int BufferSize = 8192;
try
{
client = new TcpClient();
//與伺服器建立連線
client.Connect(IPAddress.Parse("192.168.3.19"), 9322);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
//列印連線到的服務端資訊
Console.WriteLine("Server Connected! Local: {0} --> Server: {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
NetworkStream streamToServer = client.GetStream();
string msg;
do
{
Console.Write("Sent:");
msg = Console.ReadLine();
if (!string.IsNullOrEmpty(msg)&&msg!="Q")
{
byte[] buffer = Encoding.Unicode.GetBytes(msg);
try
{
//發往伺服器
streamToServer.Write(buffer,0,buffer.Length);
int bytesRead;
buffer = new byte[BufferSize];
//接受並顯示伺服器回傳的字串
bytesRead = streamToServer.Read(buffer,0,BufferSize);
if (bytesRead==0)
{
Console.WriteLine("Server offline");
break;
}
msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
break;
}
}
} while (msg!="Q");
streamToServer.Dispose();
client.Close();
}
}
}
先執行一下服務端,然後執行客戶端,就會看到相應的輸出.
這樣大家應該會對C#的網路程式設計有了一定得了解,當然了,這是隻是網路程式設計中的皮毛.因為到目前為止,我們們的操作都是同步操作,上面的程式碼只能作為入門使用.在實際中,一個服務端只能為一個客戶端提供服務的情況幾乎不存在.
下面我們們要看非同步的網路程式設計之前,先學習一下在不同的編碼方式中英文的大小,以及TCP快取導致的文字邊界問題.
相關文章
- ios檔案同步傳輸工具iOS
- 中介軟體---Binlog傳輸同步---Maxwell
- 中介軟體---Binlog傳輸同步---Canal
- FTP檔案服務搭建與同步傳輸FTP
- 資料傳輸 | 如何配合 pt-osc 使用 DTLE 同步 DDL
- 資料傳輸 | 利用 DTLE 將 MySQL 資料同步到 DBLEMySql
- Vue父子元件通過prop非同步傳輸資料踩坑Vue元件非同步
- freemark輸出字串字串
- 字串指標的輸出字串指標
- 字串倒序輸出字串
- 字串abcde我要輸出字串de?字串
- 鐳速傳輸:如何快速傳輸大檔案?
- 輸入一段字串,去除字串中重複的字元,並輸出字串字元
- 鐳速傳輸淺談TLS 和檔案傳輸TLS
- 鐳速傳輸:安全檔案傳輸的意義
- ncurses輸出函式:字元+字串的輸出函式字元字串
- ncurses輸入函式:字元+字串的輸入函式字元字串
- 05:輸出親朋字串字串
- 遞迴逆向輸出字串遞迴字串
- 安全加密傳輸加密
- node中子程式同步輸出
- 通過中轉機及ssh rsync 傳輸歸檔檔案進行同步
- 如何看待鐳速傳輸的Raysync高速傳輸協議?協議
- 【流式傳輸】使用Spring Boot實現ChatGpt流式傳輸Spring BootChatGPT
- FTP的傳輸有兩種方式:ASCII傳輸模式和二進位制資料傳輸模式FTPASCII模式
- USB 控制寫傳輸、控制讀傳輸、無資料控制傳輸都是在什麼場景下?
- 什麼是極速檔案傳輸,極速檔案傳輸如何進行大檔案傳輸
- 讀取不定長字串輸入字串
- 字串拼接格式化輸出字串
- 字串、整數倒序輸出字串
- js如何正常輸出字串</script>JS字串
- 重複輸出字元或字串字元字串
- 瞭解PCI Express的Posted傳輸與Non-Posted傳輸Express
- 【分段傳輸】c#使用IAsyncEnumerable實現流式分段傳輸C#
- Java傳輸檔案使用Base64優化傳輸速率。Java優化
- TCP可靠傳輸原理TCP
- webservice傳輸檔案Web
- sftp 傳輸檔案FTP