C#Socket程式設計詳解(三)Socket程式設計
三、Socket程式設計
1、UDP通訊
1.1採用Socket實現UDP
1.1.1簡介
Socket實現UDP的基本步驟如下:
(1)建立一個Socket物件
Socket mySocket = new Socket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp);
AddressFamily 定址型別
AddressFamily.InterNetwork代表IPV4。
SocketType 套接字型別
SocketType.Dgram表示使用資料報協議。
ProtocolType 協議型別
ProtocolType.Udp表示使用UDP協議
但需要注意的是套接字型別與協議型別並不是可以隨便組合。如下表
SocketType | ProtocolType | 描述 |
Stream(使用位元組流) | Tcp | 面向連線 |
Dgram(使用資料報) | Udp | 面向無連線 |
Raw | Icmp | 網際訊息控制 |
Raw | Raw | 基礎傳輸協議 |
(2)將建立的套接字物件與本地IPEndPoint進行繫結。
完成上述步驟後,那麼建立的套接字就能夠在本地IPEndPoint上接收流入的UDP資料包,或者將流出的UDP資料包通過本地IPEndPoint傳送到網路中其他任意裝置。
mySocket.Bind(EndPointlocalEP)//繫結本地IP
注意:
1、繫結的目的主要是繫結埠,接收方指定在本機的哪個埠接收資料。
2、並不是所有機器都需要bind:
(1)A機僅傳送資料,B機僅用來接收A傳送的資料。如下圖所示:
此時A不用繫結本機IP(也可以繫結),B必須繫結本機IP,否則報錯。
因為A只需要傳送,不需要接收,從而就不需要監聽本機的特定埠來接收UDP資料報。B機必須要繫結本機IP。因為A在SendTo時會向指定的埠傳送UDP資料報,B機需要監聽本機相應埠,才能收到A傳送過來的資料。
(2)B發訊息A接收,A接收到訊息後傳送訊息B接收。如下圖所示:
此時A必須繫結,B不需要繫結(也可以繫結)。
A需要在程式啟動時,首先監聽特定埠等待接收資訊,因此需要繫結。
B首先發送資料,不需要在指定的UDP埠等待流入的資料。B在傳送時socket會隨機指定B機的埠進行傳送。
A收到資料,同時會獲取B端的傳送IP與埠。即ReceiveFrom(byte[] data , ref EndPoint Remote)中的Remote就是B端socket傳送資料時使用的IP與埠資訊。
A再發送資料時會用收到的B機的埠進行傳送。即SendTo(byte[] data , EndPointRemote),Remote就是上面ReceiveFrom方法獲取的Remote。
此時B在接收時socket.ReceiveFrom監聽的IP與埠就是傳送資料時使用的IP與埠。
總結:
通常,接收方需要繫結埠,傳送方傳送資料時指定的埠(要傳送到接收方的哪個埠)應與接收方監聽的埠一致。
繫結的目的是為了監聽本機的接收資料的埠。傳送方傳送資料的時候會明確告知往接收方的哪個埠傳送。
(3)傳送資料
使用UDP進行通訊時,不需要連線。因為異地的主機之間沒有建立連線,所以UDP不能使用標準的Send()和Receive()套接字方法,而是使用兩個其他的方法:SendTo(byte[] data , EndPoint Remote)和ReceiveFrom(byte[] data , ref EndPoint Remote)。
SendTo()方法指定要傳送的資料,和目標機器的IPEndPoint。該方法有多種不同的使用方法,可以根據具體的應用進行選擇,但是至少要指定資料包和目標機器。如下:
SendTo(byte[] data , EndPointRemote)
<1>廣播發送:
在傳送前應對socket進行設定,如下
socket.SetSocketOption(SocketOptionLevel.Socket,SocketOptionName.Broadcast,1);
廣播訊息的目的IP地址是一種特殊IP地址,稱為廣播地址。
廣播地址由IP地址網路字首加上全1主機字尾組成,如:192.168.1.255是192.169.1.0這個網路的廣播地址;130.168.255.255是130.168.0.0這個網路的廣播地址。向全部為1的IP地址(255.255.255.255)傳送訊息的話,那麼理論上全世界所有的聯網的計算機都能收得到了。但實際上不是這樣的,一般路由器上設定拋棄這樣的包,只在本地網內廣播,所以效果和向本地網的廣播地址傳送訊息是一樣的。
通常EndPoint Remote = new IPEndPoint(IPAddress.Broadcast,9050)
其中埠號要確定好。
<2>一對一發送
一對一發送無需設定socket
EndPoint Remote需要制定接收機的IP與接收埠。
(4)接收資料
ReceiveFrom()方法同SendTo()方法類似,但是使用EndPoint物件宣告的方式不一樣。利用ref修飾。接收方接收到資料後Remote為傳送方的IP與埠資訊。
如果傳送方沒有繫結,則傳送方在傳送的時候,作業系統會隨機指定一個埠,接收方的Remote就是傳送方隨機確定的IP與埠。
ReceiveFrom()方法是一個阻塞方法,會一直等待,直到接收到資料才會往下執行程式。
int ReceiveFrom(byte[] data ,ref EndPointRemote)
int返回引數返回的是接收到的byte[]長度。例如傳送方傳送了一個長度為1024的byte[],接收方接收到了1024長度的byte[],則返回1024。
引數data的長度不能小於傳送方傳送的一包資料的長度。例如傳送方傳送了1024長度的byte[]。則data的長度應大於等於1024,否則由於長度不夠會報錯。
UDP應用不是嚴格意義上的真正的伺服器和客戶機,而是平等的關係,即沒有主與次的關係。
通常,我們都是將廣播發送方當做服務端,廣播接收方當做客戶端。
1.1.2程式示例
1.1.2.1實現廣播功能
服務端傳送廣播、客戶端接收廣播。
服務端程式碼:
static void Main(string[] args)
{
sock = newSocket(AddressFamily.InterNetwork,
SocketType.Dgram,ProtocolType.Udp);
//如果是廣播通常這樣設定傳送地址,一定要與接收端繫結的埠一致!
iep1 = newIPEndPoint(IPAddress.Broadcast, 9050);
stringhostname = Dns.GetHostName();
data =Encoding.ASCII.GetBytes(hostname);
//設定socket,否則程式報錯
sock.SetSocketOption(SocketOptionLevel.Socket,SocketOptionName.Broadcast,1);
Thread t = newThread(BroadcastMessage);
t.Start();
Console.ReadKey();
}
private static void BroadcastMessage()
{
while (true)
{
//向iep1傳送廣播資料
sock.SendTo(data, iep1);
Thread.Sleep(2000);
}
}
客戶端程式碼:
Socket sock = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
//獲取本機所有IP進行監聽
//IPAddress.Any表示本機ip,換言之,如果伺服器繫結此地址,則表示偵聽//本機所有ip對應的那個埠(本機可能有多個ip或只有一個ip)
IPEndPoint iep =new IPEndPoint(IPAddress.Any, 9050);
//繫結本機埠,用於接收UDP資料包
sock.Bind(iep);
//iep可以是任意IP,因為有多網絡卡情況的存在,在ReceiveFrom後會被賦值為傳送端的地址
EndPoint ep = (EndPoint)iep;
Console.WriteLine("Ready to receive…");
byte[] data = new byte[1024];
//是一個阻塞方法,會一直等待資料,直到接收到資料才會往下執行
//返回的地址ep是路由器地址,因為是由路由器轉發的資訊。
int recv = sock.ReceiveFrom(data, ref ep);
string stringData = Encoding.ASCII.GetString(data, 0, recv);
Console.WriteLine("received: {0} from: {1}",stringData,ep.ToString());
sock.Close();
Console.ReadKey();
1.1.2.2實現一對一通訊
伺服器端程式碼:
int recv;
byte[] data = new byte[1024];
//得到本機IP,設定TCP埠號
IPEndPoint ip = new IPEndPoint(IPAddress.Any, 8001);
Socket newsock = new Socket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp);
//繫結網路地址,這裡之所以繫結,是因為要偵聽客戶端傳送來的資料
newsock.Bind(ip);
Console.WriteLine("This is a Server, host name is{0}", Dns.GetHostName());
//等待客戶機連線
Console.WriteLine("Waiting for a client");
//得到客戶機IP
EndPoint Remote = new IPEndPoint(IPAddress.Any, 0);
//獲得的Remote是客戶端地址
recv = newsock.ReceiveFrom(data, ref Remote);
Console.WriteLine("Message received from {0}:", Remote.ToString());
Console.WriteLine(Encoding.ASCII.GetString(data, 0,recv));
//客戶機連線成功後,傳送資訊
string welcome = "你好 ! ";
//字串與位元組陣列相互轉換
data = Encoding.ASCII.GetBytes(welcome);
//傳送資訊
newsock.SendTo(data, data.Length, SocketFlags.None,Remote);
newsock.close();
客戶端程式碼:
byte[] data = new byte[1024];
string input, stringData;
Console.WriteLine("This is a Client, host name is{0}", Dns.GetHostName());
//設定為服務端IP與埠
IPEndPoint ip = new IPEndPoint(IPAddress.Parse("127.0.0.1"),8001);
//定義網路型別,資料連線型別和網路協議UDP
Socket server = new Socket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp);
string welcome = "你好! ";
data = Encoding.UTF8.GetBytes(welcome);
//向服務端傳送資料,此時客戶端通過本機哪個IP與埠傳送資料是隨機的,
//該資訊被存放在socket中。
//服務端接收到資訊會得到客戶端的傳送地址,服務端利用這個地址向客戶端
//傳送訊息,客戶端直接利用這個socket就可以接收資料,從而無需bind。
server.SendTo(data, data.Length, SocketFlags.None, ip);
EndPoint Remote = new IPEndPoint(IPAddress.Any, 0);
data = new byte[1024];
//獲得的Remote是服務端地址
int recv = server.ReceiveFrom(data, ref Remote);
Console.WriteLine("Message received from {0}:", Remote.ToString());
Console.WriteLine(Encoding.UTF8.GetString(data, 0,recv));
Console.WriteLine("Stopping Client.");
server.Close();
注意:該例子是服務端接收到客戶端的資訊後,直接利用receiveFrom獲取的客戶端IP向客戶端傳送訊息。因此,客戶端利用傳送時的IP與埠進行資訊的接收,無需bind。
如果服務端與客戶端指定了各自的接收埠,那麼雙方都需要繫結各自的埠進行監聽。舉例:
服務端(IP:172.1.1.1監聽埠:8001)
IPEndPoint Local = new IPEndPoint(IPAddress.Any, 8001);
IPEndPoint Remote = new IPEndPoint(IPAddress. Parse("127.1.1.2"),8002);
socket.Bind(Local);
//向客戶端繫結的地址傳送資訊
socket.SendTo(data,Remote);
客戶端(IP:172.1.1.2 監聽埠:8002)
IPEndPoint Local = new IPEndPoint(IPAddress.Any, 8001);
IPEndPoint Remote = new IPEndPoint(IPAddress. Parse("127.1.1.2"),8002);
socket.Bind(Local);
//向服務端繫結的地址傳送資訊
socket.SendTo(data,Remote);
一旦socket綁定了IP與埠,那麼會通過該埠收發資料。
如果想傳送與接收繫結不同的地址,則需要在一個程式中宣告兩個socket,繫結不同的地址,一個socket用於收,一個socket用於發。
1.1.2.3實現一對多通訊(組播)
IP組播通訊需要一個特殊的組播地址,IP組播地址是一組D類IP地址,範圍從224.0.0.0 到239.255.255.255。其中還有很多地址是為特殊的目的保留的。224.0.0.0到224.0.0.255的地址最好不要用,因為他們大多是為了特殊的目的保持的(比如IGMP協議)。
IGMP是IP組播的基礎。在IP協議出現以後為了加入對組播的支援,IGMP產生了。IGMP所做的實際上就是告訴路由器,在這個路由器所在的子網內有人對傳送到某一個組播組的資料感興趣,這樣當這個組播組的資料到達後面,路由器就不會拋棄它,而是把他轉送給所有感興趣的客戶。假如不同子網內的A和B要進行組播通訊,那麼位於AB之間的所有路由器必須都要支援IGMP協議,否則AB之間不能進行通訊。
1 使用Socket實現任意源組播
利用C#實現UDP組播的基本步驟為:
(1)建立socket;
(2)socket和埠繫結;
(3)加入一個組播組;
(4)通過sendto / recvfrom進行資料的收發;
(5)關閉socket。
下面是簡單的示例:
(1) 傳送示例:
byte[] data = new byte[1024];
string hostname = Dns.GetHostName();
string welcome = hostname+ ":你好 ! ";
data = Encoding.UTF8.GetBytes(welcome);
//宣告組播組IP
IPAddress ip = IPAddress.Parse("226.1.1.2");
Socket s = new Socket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp);
//對socket進行設定
s.SetSocketOption(SocketOptionLevel.IP,SocketOptionName.MulticastTimeToLive, 1);
//確定組播地址,注意埠號要與接收端統一
IPEndPoint ipep = new IPEndPoint(ip, 5000);
while (true)
{
Thread.Sleep(2000);
s.SendTo(data,data.Length, SocketFlags.None, ipep);
}
s.Close();
(2)接收示例:
byte[] data = new byte[1024];
int recv = 0;
Socket s = new Socket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp);
//繫結本機地址,注意埠號要與傳送端多播埠號一致
IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 5000);
s.Bind(ipep);
//加入到多播組
s.SetSocketOption(SocketOptionLevel.IP,SocketOptionName.AddMembership,
newMulticastOption(IPAddress.Parse("226.1.1.2"), IPAddress.Any));
//定義任意地址用於返回傳送方的地址
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
EndPoint Remote = (EndPoint)sender;
while (true)
{
//放回的是路由器的地址,而不是傳送機器的地址,這是因為是由路由器轉發的
//資訊
recv =s.ReceiveFrom(data, ref Remote);
Console.WriteLine(Encoding.UTF8.GetString(data, 0, recv));
}
s.Close();
Console.ReadKey();
1.2採用UdpClient實現UDP
1.2.1簡介
(1)建立物件
繫結指定的埠進行資料的接收與傳送:
UdpClient udpClient= new UdpClient(11000);
使用隨機的埠進行資料的接收與傳送:
UdpClient udpClient= new UdpClient(0);
使用隨機埠進行傳送,每次發出去的資訊埠號碼都是不同的,這樣接收方要把資訊發回來,就沒辦法了。
繫結指定終結點,進行資料的傳送:
UdpClient udpClient= new UdpClient(IPEndPoint);
(2)傳送資料
傳送的時候建立一個結點,來指定接收方的地址
Send(byte[],Int32,IpEndPoint)
Send(byte[],Int32,String,Int32)
Send(byte[],Int32)
在使用第三個send方法時由於沒有指定遠端地址,需要在之前使用Connect方法來指定遠端地址。
(3)接收資訊
byte[] Receive(IpEndPoint)
該方法是一個阻塞方法,如果沒有收到資料,會一直在該方法上等待。
1.2.2程式示例
1.1.2.1實現廣播功能
傳送端:
其實UDP廣播就是向255.255.255.255傳送資料,接收端只需繫結UDP廣播的埠號即可。傳送端,傳送的地址,255.255.255.255:Port,即,IPAddress.Broadcast:Port。
UdpClient UDPsend = new UdpClient(newIPEndPoint(IPAddress.Any, 0));
//其實IPAddress.Broadcast 就是 255.255.255.255
IPEndPoint endpoint = newIPEndPoint(IPAddress.Broadcast, 8080);
byte[] buf = Encoding.Default.GetBytes("This is UDPbroadcast");
UDPsend.Send(buf, buf.Length, endpoint);
接收端:
接收端僅需繫結傳送端傳送時指定的埠,便可以接收廣播資訊
IPEndPoint LocalIp = new IPEndPoint(IPAddress.Any, 8080);
//任意地址,用於Receive中的ref引數,返回傳送端的地址。
IPEndPoint Remote = new IPEndPoint(IPAddress.Any, 0);
UdpClient udpClient = new UdpClient(LocalIp);
//Receive方法返回byte陣列。
byte[] d = udpClient.Receive(ref Remote);
string s = Encoding.UTF8.GetString(d, 0, d.Length);
1.1.2.2實現一對一通訊
傳送端:
//定義接收端的地址
IPEndPoint targetIp = newIPEndPoint(IPAddress.Parse("192.168.2.119"), 8001);
UdpClient udpClient= new UdpClient(0);
byte[] data = Encoding.UTF8.GetBytes(“Hello”);
//傳送資訊,第二個引數為傳送的位元組數
udpClient.Send(data, data.Length, targetIp);
接收端:
//定義本地地址,用於不斷監聽該地址收到的資訊。該地址與傳送端的傳送地址//一致。
IPEndPoint LocalIp = new IPEndPoint(IPAddress.Any,8001);
//任意地址,用於Receive中的ref引數,返回傳送端的地址。
IPEndPoint Remote = new IPEndPoint(IPAddress.Any, 0);
UdpClient udpClient = new UdpClient(LocalIp);
//Receive方法返回byte陣列。
byte[] d = udpClient.Receive(ref Remote);
string s = Encoding.UTF8.GetString(d, 0, d.Length);
1.1.2.3實現一對多通訊(組播)
IP組播通訊需要一個特殊的組播地址,IP組播地址是一組D類IP地址,範圍從224.0.0.0 到239.255.255.255。其中還有很多地址是為特殊的目的保留的。224.0.0.0到224.0.0.255的地址最好不要用,因為他們大多是為了特殊的目的保持的(比如IGMP協議)。
因此如果設定的IP不符合D類IP規則,則程式報錯。
傳送端:
//定義組播地址,注意一定要是D類廣播
IPEndPoint targetIp = new IPEndPoint(IPAddress.Parse("226.1.1.2"),8001);
UdpClient udpClient= new UdpClient(0);
byte[] data = Encoding.UTF8.GetBytes(“Hello”);
//傳送資訊,第二個引數為傳送的位元組數
udpClient.Send(data, data.Length, targetIp);
接收端:
//定義本地地址,注意埠一定與傳送端傳送時指定的埠一致
IPEndPoint LocalIp = new IPEndPoint(IPAddress.Any,8001);
//任意地址,用於Receive中的ref引數,返回傳送端的地址。
IPEndPoint Remote = new IPEndPoint(IPAddress.Any, 0);
UdpClient udpClient = new UdpClient(LocalIp);
//加入組播,與傳送端定義的組播地址一致,必須符合D類IP地址,否則報錯
udpClient.JoinMulticastGroup(IPAddress.Parse("226.1.1.2"));
//Receive方法返回byte陣列。
byte[] d = udpClient.Receive(ref Remote);
string s = Encoding.UTF8.GetString(d, 0, d.Length);
//離開該多播組
udpClient.DropMulticastGroup(IPAddress.Parse("226.1.1.2"));
2、TCP通訊
2.1採用Socket實現TCP
2.1.1同步通訊
2.1.1.1簡介
服務端:
服務端的主要職責是處理各個客戶端傳送來的資料,因此在客戶端的Socket程式設計中需要使用兩個執行緒來迴圈處理客戶端的請求,一個執行緒用於監聽客戶端的連線情況,一個執行緒用於監聽客戶端的訊息傳送。
基本流程
• 建立套接字
• 繫結套接字的IP和埠號——Bind(EndPoint)
• 將套接字處於監聽狀態等待客戶端的連線請求——Listen(int) 監聽埠,其中parameters表示最大監聽數。
• 阻塞方法,當請求到來後,接受請求並返回本次會話的套接字——Accept()接受客戶端連結,並返回一個新的連結,用於處理同客戶端的通訊問題。
• 使用返回的套接字和客戶端通訊——Send()/Receive()
Send(byte[]) 簡單傳送資料。
Send(byte[],SocketFlag) 使用指定的SocketFlag傳送資料。
Send(byte[],int, SocketFlag) 使用指定的SocketFlag傳送指定長度資料。
Send(byte[], int, int,SocketFlag) 使用指定的SocketFlag,將指定位元組數的資料傳送到已連線的socket(從指定偏移量開始)。
Receive(byte[]) 簡單接受資料。
Receive(byte[],SocketFlag) 使用指定的SocketFlag接受資料。
Receive(byte[], int, SocketFlag) 使用指定的SocketFlag接受指定長度資料。
Receive (byte[], int,int, SocketFlag) 使用指定的SocketFlag,從繫結的套接字接收指定位元組數的資料,並存到指定偏移量位置的緩衝區。
• 返回,再次等待新的連線請求
• 關閉套接字
客戶端:
客戶端相對於服務端來說任務要輕許多,因為客戶端僅僅需要和服務端通訊即可,可是因為在和伺服器通訊的過程中,需要時刻保持連線通暢,因此同樣需要兩個執行緒來分別處理連線情況的監聽和訊息傳送的監聽。
基本流程
• 建立套接字保證與伺服器的埠一致
• 向伺服器發出連線請求——Connect(EndPoint)連線遠端伺服器,如果沒有相應的遠端等待連線,直接返回錯誤。
• 和伺服器端進行通訊——Send()/Receive()
• 關閉套接字
2.1.1.2程式示例
2.1.1.2.1簡單的單次通訊
服務端接收客戶端資訊並向客戶端返回資訊
服務端:
static void Main()
{
try{
IPEndPoint ipe = new IPEndPoint(IPAddress.Any,8001);
Socket s = newSocket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp);//建立一個Socket類
s.Bind(ipe);//繫結
s.Listen(0);//開始監聽
Socket temp = s.Accept();//等待客戶端連線
byte[] recvBytes = new byte[1024];
int bytes = temp.Receive(recvBytes);//從客戶端接受資訊,返回接收的資料長度
string recvStr = Encoding.UTF8.GetString(recvBytes,0, bytes);
Console.WriteLine("Server GetMessage:{0}", recvStr);
string sendStr = "Ok!Client SendMessage Sucessful!";
byte[] bs = Encoding.UTF8.GetBytes(sendStr);
temp.Send(bs);//向客戶端傳送資料
temp.Close();
s.Close();
}
catch (ArgumentNullException e){
Console.WriteLine("ArgumentNullException:{0}", e);}
catch (SocketException e){
Console.WriteLine("SocketException: {0}", e);}
Console.WriteLine("Press Enter to Exit");
Console.ReadLine();
}
客戶端:
static void Main()
{
try{
IPEndPoint ipe =new IPEndPoint IPAddress.Parse(“192.168.2.119”),8001);//伺服器地址
Socket c = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);//建立一個Socket
c.Connect(ipe);//連線到伺服器
byte[] bs =Encoding.UTF8.GetBytes("hello!This is a socket test");
c.Send(bs);//向服務端傳送資訊
byte[] recvBytes= new byte[1024];
int bytes= c.Receive(recvBytes);//從伺服器端接受返回資訊
string recvStr +=Encoding.UTF8.GetString(recvBytes, 0, bytes);
Console.WriteLine("Client Get Message:{0}", recvStr);//顯示伺服器返回資訊
c.Close();
}
catch(ArgumentNullException e){
Console.WriteLine("ArgumentNullException:{0}", e);
}
catch(SocketException e){
Console.WriteLine("SocketException:{0}", e);
}
Console.WriteLine("Press Enter to Exit");
Console.ReadLine();
}
2.1.1.2.1多次通訊
public class TCPServer
{
private byte[]result = new byte[1024];
// 最大的監聽數量
privateint maxClientCount;
// IP地址
privatestring ip;
// 埠號
privateint port;
// 客戶端列表
privateList<Socket> ClientSockets;
// IP終端
privateIPEndPoint ipEndPoint;
// 服務端Socket
privateSocket ServerSocket;
// 當前客戶端Socket
privateSocket ClientSocket;
public TCPServer(int port, int count)
{
this.ip =IPAddress.Any.ToString();
this.port = port;
this.maxClientCount=count;
this.ClientSockets = newList<Socket>();
//初始化IP終端
this.ipEndPoint = newIPEndPoint(IPAddress.Any, port);
//初始化服務端Socket
this.ServerSocket = newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//埠繫結
this.mServerSocket.Bind(this.ipEndPoint);
//設定監聽數目
this.ServerSocket.Listen(maxClientCount);
}
// 定義一個Start方法將建構函式中的方法分離出來
public void Start()
{
//建立服務端執行緒,實現客戶端連線請求的迴圈監聽
var ServerThread = new Thread(this.ListenClientConnect);
//服務端執行緒開啟
ServerThread.Start();
}
// 監聽客戶端連結
private void ListenClientConnect()
{
//設定迴圈標誌位
while(true)
{
//獲取連線到服務端的客戶端
this.ClientSocket = this.ServerSocket.Accept();
//將獲取到的客戶端新增到客戶端列表
this.ClientSockets.Add(this.ClientSocket);
//向客戶端傳送一條訊息
this.SendMessage(string.Format("客戶端{0}已成功連線到伺服器",this.ClientSocket.RemoteEndPoint));
//建立客戶端訊息執行緒,實現客戶端訊息的迴圈監聽
var ReveiveThread = newThread(this.ReceiveClient);
//注意到ReceiveClient方法傳入了一個引數
//實際上這個引數就是此時連線到伺服器的客戶端
//即ClientSocket
ReveiveThread.Start(this.ClientSocket);
}
}
// 接收客戶端訊息的方法
private void ReceiveClient(objectobj)
{
Socket_ClientSocket = (Socket)obj;
while(true)
{
try
{
//獲取資料長度
int receiveLength = _ClientSocket.Receive(result);
//獲取客戶端訊息
string clientMessage =Encoding.UTF8.GetString(result, 0, receiveLength);
//服務端負責將客戶端的訊息分發給各個客戶端
this.SendMessage(string.Format("客戶端{0}發來訊息:{1}",_ClientSocket.RemoteEndPoint,clientMessage));
}
catch (Exception e)
{
//從客戶端列表中移除該客戶端
this.ClientSockets.Remove(_ClientSocket);
//向其它客戶端告知該客戶端下線
this.SendMessage(string.Format("伺服器發來訊息:客戶端{0}從伺服器斷開,斷開原因{1}",_ClientSocket.RemoteEndPoint,e.Message));
//斷開連線
_ClientSocket.Shutdown(SocketShutdown.Both);
_ClientSocket.Close();
break;
}
}
}
// 向所有的客戶端群發訊息
public void SendMessage(string msg)
{
//確保訊息非空以及客戶端列表非空
if (msg == string.Empty || this.ClientSockets.Count<= 0) return;
//向每一個客戶端傳送訊息
foreach (Socket s in this.ClientSockets)
{
s.Send(Encoding.UTF8.GetBytes(msg));
}
}
// 向指定的客戶端傳送訊息
public void SendMessage(string ip,intport,string msg)
{
//構造出一個終端地址
IPEndPoint_IPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
//遍歷所有客戶端
foreach (Socket s in ClientSockets)
{
if (_IPEndPoint ==(IPEndPoint)s.RemoteEndPoint)
{
s.Send(Encoding.UTF8.GetBytes(msg));
}
}
}
}
2.1.2非同步通訊
2.1.1.1簡介
(1)BeginAccept(AsynscCallBack,object)
開始一個非同步的接受一個連線嘗試。
object:通常傳入的是服務Socket物件例項。
AsynscCallBack:回撥函式,該回調函式中必須包含EndAccept方法。
應用程式呼叫BegineAccept方法後,系統會使用單獨的執行緒執行指定的回撥方法並在EndAccept上一直處於阻塞狀態,直至監測到掛起的連結。EndAccept會返回新的socket物件。供你來同遠端主機資料互動。呼叫BeginAccept當希望原始執行緒阻塞的時候,請呼叫WaitHandle.WaitOne方法。當需要原始執行緒繼續執行時請在回撥方法中使用ManualResetEvent的set方法。
(2)Socket EndAccept(IAsyncResult)
重點是返回一個Socket與客戶端進行通訊。IAsyncResult作用不大。
(3)BeginConnect(EndPoint,AsyncCallBack, Object)
EndPoint:指定想要連線的服務端地址。
AsynscCallBack:回撥函式,該回調函式中必須包含EndConnect方法。
object:通常傳入的是客戶端Socket物件例項。
(4)void EndConnect(IAsyncResult)
無返回值,在為連線成功前一直在該方法上等待。
(5)BeginReceive(byte[] buffer,int offset, int size, SocketFlags, AsyncCallbac, object)
buffer:用於存放接收資訊。
offset:指定buffer的其實儲存位置。
size:指定接收的資訊byte長度。
SocketFlags:socket標誌位。
AsynscCallBack:回撥函式,該回調函式中必須包含EndReceive方法。
object:通常是一個存放了第一個引數buffer以及與遠端進行通訊的Socket物件的資料結構的物件例項。
(6)int EndReceieve(IAsyncResult)
返回收到的資訊的byte陣列長度。IAsyncResult作用不大。
(7)BeginSend(byte[],SocketFlag, AsyncCallBack, Object)
byte[]:需要傳送的資料。
AsyncCallBack:回撥函式,該回調函式中必須包含EndSend方法。
object:通常為與遠端進行通訊的Socket例項。
(8)int EndSend(IAsyncResult)
返回傳送的資訊的byte陣列長度。IAsyncResult作用不大。
服務端:
基本流程
• 建立套接字
• 繫結套接字的IP和埠號——Bind()
• 使套接字處於監聽狀態等待客戶端的連線請求——Listen()
• 當請求到來後,使用BeginAccept()和EndAccept()方法接受請求,返回新的套接字
• 使用BeginSend()/EndSend和BeginReceive()/EndReceive()兩組方法與客戶端進行收發通訊
• 關閉套接字
客戶端:
基本流程
• 建立套接字並保證與伺服器的埠一致
• 使用BeginConnect()和EndConnect()這組方法向服務端傳送連線請求
• 使用BeginSend()/EndSend和BeginReceive()/EndReceive()兩組方法與服務端進行收發通訊
• 關閉套接字
2.1.1.2程式示例
2.1.1.2.1服務端僅用來不斷接收客戶端資訊
classStateObject
{
public Socket workSocket = null;
public const int BufferSize = 1024;
public byte[] buffer = new byte[BufferSize];
}
classAsyncServer
{
private IPEndPoint ipEndPoint;
private Socket serverSocket;
private Socket clientSocket;
private List<Socket> clients;
private ManualResetEvent AcceptDone;
public AsyncServer()
{
clients = new List<Socket>();
ipEndPoint = new IPEndPoint(IPAddress.Any,8001);
serverSocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(ipEndPoint);
serverSocket.Listen(10);
AcceptDone = new ManualResetEvent(false);
}
//啟動函式
public void Start()
{
Thread startT = new Thread(ListenThread);
startT.Start();
}
public void ListenThread()
{
while (true)
{
AcceptDone.Reset();
serverSocket.BeginAccept(newAsyncCallback(AcceptCallback), serverSocket);
AcceptDone.WaitOne();
}
}
public void AcceptCallback(IAsyncResult ar)
{
Socket temp =((Socket)ar.AsyncState).EndAccept(ar);
StateObject state = new StateObject();
state.workSocket = temp;
temp.BeginReceive(state.buffer, 0, StateObject.BufferSize,0, new AsyncCallback(ReceiveCallback), state);
AcceptDone.Set();
}
public void ReceiveCallback(IAsyncResult ar)
{
StateObject state =(StateObject)ar.AsyncState;
Socket handler = state.workSocket;
int i = handler.EndReceive(ar);
if (i > 0)
{
string message =Encoding.UTF8.GetString(state.buffer, 0, i);
Console.WriteLine(message);
}
}
}
2.1.1.2.2服務端與客戶端雙向通訊
(該程式碼引用自他人)
非同步通訊:
客戶端Client:
預定義結構體,用於非同步委託之間的傳遞。使用者根據自己需要定製即可
publicclassStateObject
{
publicSocket workSocket= null;
publicconstint BufferSize= 256;
publicbyte[] buffer= newbyte[BufferSize];
publicStringBuilder sb = newStringBuilder();
}
正文:
publicclassAsynchronousClient
{
privateconstint port =11000;
privatestaticManualResetEvent connectDone= newManualResetEvent(false);
privatestaticManualResetEvent sendDone= newManualResetEvent(false);
privatestaticManualResetEvent receiveDone= newManualResetEvent(false);
privatestaticString response= String.Empty;
privatestaticvoid StartClient(){
try{
IPHostEntry ipHostInfo= Dns.Resolve("host.contoso.com");
IPAddress ipAddress= ipHostInfo.AddressList[0];
IPEndPoint remoteEP= newIPEndPoint(ipAddress,port);
Socket client= newSocket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
client.BeginConnect(remoteEP,newAsyncCallback(ConnectCallback), client);
connectDone.WaitOne();
Send(client, <