從socket開始學習網路程式設計
1 URL快速下載(上傳)。使用WebClinet對URL進行瀏覽並下載,可以說程式碼清晰、支援豐富。包括編碼格式、下載格式、非同步下載、Form上傳、引數拼接等等各種。
WebClient mywebclient = new WebClient(); //下載為字串 var websiteString = mywebclient.DownloadString("123.com"); //下載為二進位制 var websitedata = mywebclient.DownloadData("123.com"); //非同步下載並監聽完成 mywebclient.DownloadStringCompleted += (Object Sender,DownloadStringCompletedEventArgs e) =>{ Console.Write(""); };
2 HTTP請求構造。在很多場景中,需要偽造Referer、UserAgent、ContentType等等,從一個語言的HTTP庫對HTTP協議的支援細膩程度可以看出其是否親爬蟲,幸運的是,HttpWebRequest確實足夠全面,能夠滿足所有的自定義需求。
var myhttpWebRequset = (HttpWebRequest)WebRequest.Create(new Uri(@"htttp://www.baidu.com")); myhttpWebRequset.Method = "post"; myhttpWebRequset.ContentType = "application/x-www-form-urlencoded"; myhttpWebRequset.ContentLength = 100; myhttpWebRequset.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322)";
3 Cookies處理。雖然Cookies已經逐漸淡出歷史的舞臺,但依然有大量的Web開發框架是以Cookie為支撐做Session體系的,所以Cookie的靈活操作也非常重要。
var myhttpWebRequset = (HttpWebRequest)WebRequest.Create(new Uri(@"htttp://www.baidu.com")); myhttpWebRequset.Referer = @"http://www.baidu.com"; var cookieContainer = new CookieContainer(); cookieContainer.Add(new Uri(@"http://baidu.com"), new Cookie("key", "1")); myhttpWebRequset.CookieContainer = cookieContainer;
4 代理服務。有時候目標伺服器會對IP訪問做限制,這時候使用代理伺服器以及不停的更換代理伺服器就非常重要了,如下處理也很簡潔
var mywebProxy = new WebProxy(new Uri(@"127.0.0.1:8080"));
myhttpWebRequset.Proxy = mywebProxy;
socket概念
Socket的中文釋義稱為套接字,是支撐TCP/IP通訊最基本的操作單元。可以將Socket看做不同主機之間的程序進行雙向通訊的端點,在一個雙方都可以通訊的Socket例項中,既儲存了對方的IP地址和埠,也保持了雙方通訊採用的協議等資訊。
①.流套接字:實現面向連線的TCP通訊
②.資料報套接字:實現無連線的UDP通訊
③.原始套接字:實現IP資料包的通訊(這裡不做討論)
埠
網路協議中使用埠號識別主機上不同的程序。
Tcp
TCP是一種面向連線的、可靠的,基於位元組流的傳輸層通訊協議。
TCP的工作過程
TCP是面向連線的協議,TCP協議通過三個報文段完成類似電話呼叫的連線建立過程,這個過程稱為三次握手
第一次握手:建立連線時,客戶端傳送SYN包(SEQ=x)到伺服器,並進入SYN_SEND狀態,等待伺服器確認。
第二次握手:伺服器收到SYN包,必須確認客戶的SYN(ACK=x+1),同時自己也傳送一個SYN包(SEQ=y),即SYN+ACK包,此時伺服器進入SYN_RECV狀態。
第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ACK=y+1),此包傳送完畢,客戶端和伺服器進入Established狀態,完成三次握手。
傳輸資料
一旦通訊雙方建立了TCP連線,連線中的任何一方都能向對方傳送資料和接收對方發來的資料。TCP協議負責把使用者資料(位元組流)按一定的格式和長度組成多個數據報進行傳送,並在接收到資料報之後按分解順序重新組裝和恢復使用者資料。
利用TCP傳輸資料時,資料是以位元組流的形式進行傳輸的。
tcp協議負責把資料按格式和長度傳送並接受後組裝。
連線的終止
建立一個連線需要三次握手,而終止一個連線要經過四次握手,這是由TCP的半關閉(half-close)造成的。
TCP最主要的特點如下。
(1) 是面向連線的協議。
(2) 端到端的通訊。每個TCP連線只能有兩個端點,而且只能一對一通訊,不能一點對多點直接通訊。
(3) 高可靠性。通過TCP連線傳送的資料,能保證資料無差錯、不丟失、不重複地準確到達接收方,並且保證各資料到達的順序與其發出的順序相同。
(4) 全雙工方式傳輸。
(5) 資料以位元組流的方式傳輸。
(6) 傳輸的資料無訊息邊界。
同步與非同步
同步工作方式是指利用TCP編寫的程式執行到監聽或接收語句時,在未完成工作(偵聽到連線請求或收到對方發來的資料)前不再繼續往下執行,執行緒處於阻塞狀態,直到該語句完成相應的工作後才繼續執行下一條語句。
非同步工作方式是指程式執行到監聽或接收語句時,不論工作是否完成,都會繼續往下執行。
UDP是一種簡單的、面向資料報的無連線的協議,提供的是不一定可靠的傳輸服務。所謂“無連線”是指在正式通訊前不必與對方先建立連線,不管對方狀態如何都直接傳送過去。這與發手機簡訊非常相似,只要知道對方的手機號就可以了,不要考慮對方手機處於什麼狀態。UDP雖然不能保證資料傳輸的可靠性,但資料傳輸的效率較高。
UDP與TCP的區別
(1) UDP可靠性不如TCP
TCP包含了專門的傳遞保證機制,當資料接收方收到傳送方傳來的資訊時,會自動向傳送方發出確認訊息;傳送方只有在接收到該確認訊息之後才繼續傳送其他資訊,否則將一直等待直到收到確認資訊為止。與TCP不同,UDP並不提供資料傳送的保證機制。如果在從傳送方到接收方的傳遞過程中出現數據報的丟失,協議本身並不能做出任何檢測或提示。因此,通常人們把UDP稱為不可靠的傳輸協議。
(2) UDP不能保證有序傳輸
UDP不能確保資料的傳送和接收順序。對於突發性的資料報,有可能會亂序。
UDP的優勢
- UDP速度比TCP快
由於UDP不需要先與對方建立連線,也不需要傳輸確認,因此其資料傳輸速度比TCP快得多。對於強調傳輸效能而不是傳輸完整性的應用(比如網路音訊播放、視訊點播和網路會議等),使用UDP比較合適,因為它的傳輸速度快,使通過網路播放的視訊音質好、畫面清晰。
(2) UDP有訊息邊界
傳送方UDP對應用程式交下來的報文,在新增首部後就向下直接交付給IP層。既不拆分,也不合並,而是保留這些報文的邊界。使用UDP不需要考慮訊息邊界問題,這樣使得UDP程式設計相比TCP,在對接收到的資料的處理方面要方便的多。在程式設計師看來,UDP套接字使用比TCP簡單。UDP的這一特徵也說明了它是一種面向報文的傳輸協議。
(3) UDP可以一對多傳輸
由於傳輸資料不建立連線,也就不需要維護連線狀態(包括收發狀態等),因此一臺伺服器可以同時向多個客戶端傳輸相同的訊息。利用UDP可以使用廣播或組播的方式同時向子網上的所有客戶程序傳送訊息,這一點也比TCP方便。
其中,速度快是UDP的首要優勢
由於TCP協議中植入了各種安全保障功能,在實際執行的過程中會佔用大量的系統開銷,無疑使速度受到嚴重影響。反觀UDP,由於拋棄了資訊可靠傳輸機制,將安全和排序等功能移交給上層應用完成,極大地降低了執行時間,使速度得到了保證。簡而言之,UDP的“理念”就是“不顧一切,只為更快地傳送資料”。
根據socket通訊基本流程圖,總結通訊的基本步驟:
伺服器端:
第一步:建立一個用於監聽連線的Socket對像;
第二步:用指定的埠號和伺服器的ip建立一個EndPoint對像;
第三步:用socket對像的Bind()方法繫結EndPoint;
第四步:用socket對像的Listen()方法開始監聽;
第五步:接收到客戶端的連線,用socket對像的Accept()方法建立一個新的用於和客戶端進行通訊的socket對像;
第六步:通訊結束後一定記得關閉socket;
客戶端:
第一步:建立一個Socket對像;
第二步:用指定的埠號和伺服器的ip建立一個EndPoint對像;
第三步:用socket對像的Connect()方法以上面建立的EndPoint對像做為引數,向伺服器發出連線請求;
第四步:如果連線成功,就用socket對像的Send()方法向伺服器傳送資訊;
第五步:用socket對像的Receive()方法接受伺服器發來的資訊 ;
第六步:通訊結束後一定記得關閉socket;
在服務端
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//定義回撥:解決跨執行緒訪問問題
private delegate void SetTextValueCallBack(string strValue);
//定義接收客戶端傳送訊息的回撥
private delegate void ReceiveMsgCallBack(string strReceive);
//宣告回撥
private SetTextValueCallBack setCallBack;
//宣告
private ReceiveMsgCallBack receiveCallBack;
//定義回撥:給ComboBox控制元件新增元素
private delegate void SetCmbCallBack(string strItem);
//宣告
private SetCmbCallBack setCmbCallBack;
//定義傳送檔案的回撥
private delegate void SendFileCallBack(byte[] bf);
//宣告
private SendFileCallBack sendCallBack;
//用於通訊的Socket
Socket socketSend;
//用於監聽的SOCKET
Socket socketWatch;
//將遠端連線的客戶端的IP地址和Socket存入集合中
Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();
//建立監聽連線的執行緒
Thread AcceptSocketThread;
//接收客戶端傳送訊息的執行緒
Thread threadReceive;
private void btn_Start_Click(object sender, EventArgs e)
{
//當點選開始監聽的時候 在伺服器端建立一個負責監聽IP地址和埠號的Socket
socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//獲取ip地址
IPAddress ip = IPAddress.Parse(this.txt_IP.Text.Trim());
//建立埠號
IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(this.txt_Port.Text.Trim()));
//繫結IP地址和埠號
socketWatch.Bind(point);
this.txt_Log.AppendText("監聽成功" + " \r \n");
//開始監聽:設定最大可以同時連線多少個請求
socketWatch.Listen(10);
setCallBack = new SetTextValueCallBack(SetTextValue);
receiveCallBack = new ReceiveMsgCallBack(ReceiveMsg);
setCmbCallBack = new SetCmbCallBack(AddCmbItem);
sendCallBack = new SendFileCallBack(SendFile);
//建立執行緒
AcceptSocketThread = new Thread(new ParameterizedThreadStart(StartListen));
}
/// <summary>
/// 等待客戶端的連線,並且建立與之通訊用的Socket
/// </summary>
/// <param name="obj"></param>
private void StartListen(object obj)
{
Socket socketWatch = obj as Socket;
while (true)
{
//等待客戶端的連線,並且建立一個用於通訊的Socket
socketSend = socketWatch.Accept();
//獲取遠端主機的ip地址和埠號
string strIp = socketSend.RemoteEndPoint.ToString();
dicSocket.Add(strIp, socketSend);
this.cmb_Socket.Invoke(setCmbCallBack, strIp);
string strMsg = "遠端主機:" + socketSend.RemoteEndPoint + "連線成功";
//使用回撥
txt_Log.Invoke(setCallBack, strMsg);
//定義接收客戶端訊息的執行緒
Thread threadReceive = new Thread(new ParameterizedThreadStart(Receive));
threadReceive.IsBackground = true;
threadReceive.Start(socketSend);
}
}
/// <summary>
/// 伺服器端不停的接收客戶端傳送的訊息
/// </summary>
/// <param name="obj"></param>
private void Receive(object obj)
{
Socket socketSend = obj as Socket;
while (true)
{
//客戶端連線成功後,伺服器接收客戶端傳送的訊息
byte[] buffer = new byte[2048];
//實際接收到的有效位元組數
int count = socketSend.Receive(buffer);
if (count == 0)//count 表示客戶端關閉,要退出迴圈
{
break;
}
else
{
string str = Encoding.Default.GetString(buffer, 0, count);
string strReceiveMsg = "接收:" + socketSend.RemoteEndPoint + "傳送的訊息:" + str;
txt_Log.Invoke(receiveCallBack, strReceiveMsg);
}
}
}
/// <summary>
/// 回撥委託需要執行的方法
/// </summary>
/// <param name="strValue"></param>
private void SetTextValue(string strValue)
{
this.txt_Log.AppendText(strValue + " \r \n");
}
private void txt_Port_TextChanged(object sender, EventArgs e)
{
}
private void ReceiveMsg(string strMsg)
{
this.txt_Log.AppendText(strMsg + " \r \n");
}
private void AddCmbItem(string strItem)
{
this.cmb_Socket.Items.Add(strItem);
}
private void SendFile(byte[] sendBuffer)
{
try
{
dicSocket[cmb_Socket.SelectedItem.ToString()].Send(sendBuffer, SocketFlags.None);
}
catch (Exception ex)
{
MessageBox.Show("傳送檔案出錯:"+ex.Message);
}
}
/// 伺服器給客戶端傳送訊息
private void btn_Send_Click_Click(object sender, EventArgs e)
{
try
{
string strMsg = this.txt_Msg.Text.Trim();
byte[] buffer = Encoding.Default.GetBytes(strMsg);
List<byte> list = new List<byte>();
list.Add(0);
list.AddRange(buffer);
//將泛型集合轉換為陣列
byte[] newBuffer = list.ToArray();
//獲得使用者選擇的IP地址
string ip = this.cmb_Socket.SelectedItem.ToString();
dicSocket[ip].Send(newBuffer);
}
catch (Exception ex)
{
}
}
private void btn_StopListen_Click_1(object sender, EventArgs e)
{
socketWatch.Close();
socketSend.Close();
//終止執行緒
AcceptSocketThread.Abort();
threadReceive.Abort();
}
}
2 在客戶端
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//建立連線的Socket
Socket socketSend;
//建立接收客戶端傳送訊息的執行緒
Thread threadReceive;
/// <summary>
/// 回撥委託需要執行的方法
/// </summary>
/// <param name="strValue"></param>
private void SetTextValue(string strValue)
{
this.txt_Log.AppendText(strValue + " \r \n");
}
private void ReceiveMsg(string strMsg)
{
this.txt_Log.AppendText(strMsg + " \r \n");
}
//連線
private void btn_Connect_Click(object sender, EventArgs e)
{
socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ip = IPAddress.Parse(this.txt_IP.Text.Trim());
socketSend.Connect(ip, Convert.ToInt32(this.txt_Port.Text.Trim()));
this.txt_Log.Invoke(new MethodInvoker(()=> { SetTextValue("連線成功"); }));
//開啟一個新的執行緒不停的接收伺服器傳送訊息的執行緒
threadReceive = new Thread(new ThreadStart(Receive));
//設定為後臺執行緒
threadReceive.IsBackground = true;
threadReceive.Start();
}
/// 介面伺服器傳送的訊息
private void Receive()
{
while (true) {
byte[] buffer = new byte[2048];
//實際接收到的位元組數
int r = socketSend.Receive(buffer);
if (r == 0)
{
break;
}
else
{
if (buffer[0] == 0)//表示傳送的是文字訊息
{
string str = Encoding.Default.GetString(buffer, 1, r - 1);
this.txt_Log.Invoke(new MethodInvoker(() => { SetTextValue("接收遠端伺服器:" + socketSend.RemoteEndPoint + "傳送的訊息:" + str); }));
}
}
}
}
private void btn_StopListen_Click_1(object sender, EventArgs e)
{
socketSend.Close();
//終止執行緒
threadReceive.Abort();
}
private void btn_Send_Click(object sender, EventArgs e)
{
string strMsg = this.txt_Msg.Text.Trim();
byte[] buffer = new byte[2048];
buffer = Encoding.Default.GetBytes(strMsg);
int receive = socketSend.Send(buffer);
}
}
我們可以通過TcpClient物件的GetStream()方法獲取該物件傳送和接收資料的 NetworkStream 物件:
TcpClient client = new TcpClient();
client.Connect("www.baidu.com", 8099);
NetworkStream nStream = client.GetStream();
也可以通過使用Socket來獲取 NetworkStream 物件:
NetworkStream myNetworkStream = new NetworkStream(mySocket);