目錄
期末考試考到了Socket概念的簡答題。之前做項目自己也用到過。剛好現在空閒整理一下心得筆記。我會從網絡編程的概念,優缺點和實際應用來總結。
這篇博客也參考了我們學校的教材和其他的博客。
一、Socket概述
百度百科:
網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱爲一個socket。
建立網絡通信連接至少要一對端口號(socket)。socket本質是編程接口(API),對TCP/IP的封裝,TCP/IP也要提供可供程序員做網絡開發所用的接口,這就是Socket編程接口;HTTP是轎車,提供了封裝或者顯示數據的具體形式;Socket是發動機,提供了網絡通信的能力。
Socket就是套接字,它是引用網絡連接的特殊的文件描述符,由3個基本要素組成:AddressFamily(網絡類型)、 SocketType(數據傳輸類型)及ProtocolType(採用的網絡協議)。
二、UDP收發
原理:
UDP是無連接協議,只需要知道對方的IP和Port就能進行數據的傳輸,其中IP負責定位主機Port負責定位應用。在C#中UDP網絡通信中並未使用socket類,但是他使用的UDPClient類和Socket原理類似。
特點:
- 雖然UDP協議無法保證數據可靠性,但因爲它是基於無連接的協議,能夠消除生成連接的系統延遲,所以速度比TCP更快;
- UDP既支持一對一連接,也支持一對多連接,所以,可以使用廣播的方式多地址發送,而TCP僅支持一對一通信;
- UDP與TCP的報頭比是8:20,所以相對TCP,UDP消耗更少的網絡帶寬;
- UDP傳輸的數據有消息邊界,而TCP傳輸的數據沒有消息邊界。
UDP發:
namespace UDPSend
{
public partial class Form1 : Form
{
//定義一個UDPClient類型的字段
UdpClient udpClient;
public Form1()
{
//創建一個未與指定地址或端口綁定的UDPClient實例
udpClient = new UdpClient();
InitializeComponent();
}
//發送數據
private void button1_Click(object sender, EventArgs e)
{
//臨時存儲textBox1中的數據
string temp = this.textBox1.Text;
//將textBox1中的數據(文本)轉化爲字節編碼以便發送
byte[] bData = System.Text.Encoding.UTF8.GetBytes(temp);
//向本機的13579端口發送數據(方法1)
udpClient.Send(bData, bData.Length, Dns.GetHostName(), 13579);
//向本機的13579端口發送數據(方法2)
//利用方法2,可向其他計算機端口方式數據
//udpClient.Connect(IPAddress.Parse("127.0.0.1"), 13579);
//udpClient.Send(bData, bData.Length);
}
}
}
UDP收 :
namespace UDPReceive
{
public partial class Form1 : Form
{
//定義一個UDPClient類型的字段
UdpClient udpClient;
//定義一個線程
Thread thread;
public Form1()
{
//屏蔽異常以便跨線程訪問控件
CheckForIllegalCrossThreadCalls = false;
InitializeComponent();
//創建一個與指定端口綁定的UDPClient
//實例,此端口須與發送方端口相同
udpClient = new UdpClient(13579);
}
//監聽並接收數據
private void listen()
{
//定義一個終結點,因爲此前創建的UDPClient實例已與指定端口綁定,
//所以,此處的IP地址和端口可任意設置或不設置
IPEndPoint iep = null;
while (true)
{
//獲得發送方的數據包並轉換爲指定字符類型。
//ref關鍵字使參數按引用傳遞,當控制權傳給回調用方法時,
//在方法中對參數所做的任何更改都將反映在該變量中
string sData = System.Text.Encoding.UTF8.GetString(udpClient.Receive(ref iep));
//將接收到的數據添加到listBox1的條目中
this.listBox1.Items.Add(sData);
}
}
//啓動數據接收
private void button1_Click(object sender, EventArgs e)
{
//創建一個線程以監聽並接收數據
thread = new Thread(new ThreadStart(listen));
//設置爲後臺線程,以便關閉窗體時終止線程
thread.IsBackground = true;
thread.Start();
}
//關閉窗體時終止線程
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
//終止線程
if (thread != null) thread.Abort();
}
}
}
運行結果:
三、UDP綜合實例
把收發程序結合在一起就是一個基於UDP協議的聊天程序了。(基本功能,若有需求自行完善)
namespace UDPApp
{
public partial class Form1 : Form
{
#region var
//定義一個UDPClient_send類型的字段
UdpClient udpClient;
//定義一個UDPClient_receive類型的字段
UdpClient udpClient_receive;
//定義一個線程
Thread thread;
#endregion
public Form1()
{
//創建一個未與指定地址或端口綁定的UDPClient實例
udpClient = new UdpClient();
InitializeComponent();
//屏蔽異常以便跨線程訪問控件
CheckForIllegalCrossThreadCalls = false;
}
//發送數據send
private void button1_Click(object sender, EventArgs e)
{
//臨時存儲textBox1中的數據
string temp = this.textBox1.Text;
//將textBox1中的數據(文本)轉化爲字節編碼以便發送
byte[] bData = System.Text.Encoding.UTF8.GetBytes(temp);
//向本機的13579端口發送數據(方法1)
udpClient.Send(bData, bData.Length, Dns.GetHostName(), Convert.ToUInt16(this.textsend.Text));
//向本機的13579端口發送數據(方法2)
//利用方法2,可向其他計算機端口方式數據
//udpClient.Connect(IPAddress.Parse("127.0.0.1"), 13579);
//udpClient.Send(bData, bData.Length);
this.textBox1.Text = null;
}
#region receive
private void btnStart_Click(object sender, EventArgs e)
{
//創建一個與指定端口綁定的UDPClient
//實例,此端口須與發送方端口相同
udpClient_receive = new UdpClient(Convert.ToUInt16(this.textreceive.Text));
//創建一個線程以監聽並接收數據
thread = new Thread(new ThreadStart(listen));
//設置爲後臺線程,以便關閉窗體時終止線程
thread.IsBackground = true;
thread.Start();
}
//監聽並接收數據
private void listen()
{
// DateTime currentTime = new DateTime();
//定義一個終結點,因爲此前創建的UDPClient實例已與指定端口綁定,
//所以,此處的IP地址和端口可任意設置或不設置
IPEndPoint iep = null;
while (true)
{
//獲得發送方的數據包並轉換爲指定字符類型。
//ref關鍵字使參數按引用傳遞,當控制權傳給回調用方法時,
//在方法中對參數所做的任何更改都將反映在該變量中
string sData = System.Text.Encoding.UTF8.GetString(udpClient_receive.Receive(ref iep));
//將接收到的數據添加到listBox1的條目中--回車問題
string[] ssData = sData.Split('\n');
for (int i = 0; i < ssData.Length; i++)
{
this.listBox1.Items.Add(DateTime.Now.ToString() + " 消息: " + ssData[i]);
}
// this.listBox1.Items.Add(DateTime.Now.ToString()+" 消息: "+sData);
}
}
#endregion
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
//終止線程
if (thread != null) thread.Abort();
}
}
}
UDP的接收方要一直等待監聽是否有數據發送到監聽的端口上。所以開一個線程去循環執行listen()方法。
這裏需要注意一下端口的問題,程序中我把端口固定了,再啓動一個程序是會出現端口占用的問題,所以我們要選擇不同的端口來監聽UDP。
運行結果:
四、TCP Socket
原理:
TCP基於C/S是面向連接的可靠傳輸,TCP是一種面向連接的協議,即利用TCP傳輸數據的時候,首先必須先用低級通信協議IP在計算機之間建立連接(也就是所謂的握手,然後纔可以傳輸數據),不同於UDP,TCP Socket分爲Socket和ServerSocket對應着client和server,下面我來用代碼實現一個簡單的TCP通訊功能:
客戶端:
namespace ChatClient
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;//禁用此異常
}
#region 變量
//客戶機與服務器之間的連接狀態
public bool bConnected = false;
//偵聽線程
public Thread tAcceptMsg = null;
//用於Socket通信的IP地址和端口
public IPEndPoint IPP = null;
//Socket通信
public Socket socket = null;
//網絡訪問的基礎數據流
public NetworkStream nStream = null;
//創建讀取器
public TextReader tReader = null;
//創建編寫器
public TextWriter wReader = null;
#endregion
//顯示信息
public void AcceptMessage()
{
string sTemp; //臨時存儲讀取的字符串
while (bConnected)
{
try
{
//連續從當前流中讀取字符串直至結束
sTemp = tReader.ReadLine();
if (sTemp.Length != 0)
{
//richTextBox2_KeyPress()和AcceptMessage()
//都將向richTextBox1寫字符,可能訪問有衝突,
//所以,需要多線程互斥
lock (this)
{
richTextBox1.Text = "服務器:" + sTemp + "\n" + richTextBox1.Text;
}
}
}
catch
{
MessageBox.Show("無法與服務器通信。");
}
}
//禁止當前Socket上的發送與接收
socket.Shutdown(SocketShutdown.Both);
//關閉Socket,並釋放所有關聯的資源
socket.Close();
}
//創建與服務器的連接,偵聽並顯示聊天信息
private void button1_Click(object sender, EventArgs e)
{
try
{
IPP = new IPEndPoint(IPAddress.Parse(textBox1.Text), int.Parse(textBox2.Text));
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(IPP);
if (socket.Connected)
{
nStream = new NetworkStream(socket);
tReader = new StreamReader(nStream);
wReader = new StreamWriter(nStream);
tAcceptMsg = new Thread(new ThreadStart(this.AcceptMessage));
tAcceptMsg.Start();
bConnected = true;
button1.Enabled = false;
MessageBox.Show("與服務器成功建立連接,可以通信。");
}
}
catch
{
MessageBox.Show("無法與服務器通信。");
}
}
//發送信息
private void richTextBox2_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)13)//按下的是回車鍵
{
if (bConnected)
{
try
{
//richTextBox2_KeyPress()和AcceptMessage()
//都將向richTextBox1寫字符,可能訪問有衝突,
//所以,需要多線程互斥
lock (this)
{
richTextBox1.Text = "客戶機:" + richTextBox2.Text + richTextBox1.Text;
//客戶機聊天信息寫入網絡流,以便服務器接收
wReader.WriteLine(richTextBox2.Text);
//清理當前緩衝區數據,使所有緩衝數據寫入基礎設備
wReader.Flush();
//發送成功後,清空輸入框並聚集之
richTextBox2.Text = "";
richTextBox2.Focus();
}
}
catch
{
MessageBox.Show("與服務器連接斷開。");
}
}
else
{
MessageBox.Show("未與服務器建立連接,不能通信。");
}
}
}
//關閉窗體時斷開socket連接,並終止線程(否則,VS調試程序將仍處於運行狀態)
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
socket.Close();
tAcceptMsg.Abort();
}
catch
{ }
}
}
}
首先創建一個Socket和InetSocketAddress ,然後通過Socket的connect()方法進行連接,連接成功後可以獲取到輸出流,通過該輸出流就可以向服務端傳輸數據。
服務端:
namespace ChatServer
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;//禁用此異常
}
#region 變量
//客戶機與服務器之間的連接狀態
private bool bConnected = false;
//偵聽線程
private Thread tAcceptMsg = null;
//用於Socket通信的IP地址和端口
private IPEndPoint IPP = null;
//Socket通信
private Socket socket = null;
private Socket clientSocket = null; //此處可參考,使用之前看是否分配內存
//網絡訪問的基礎數據流
private NetworkStream nStream = null;
//創建讀取器
private TextReader tReader = null;
//創建編寫器
private TextWriter wReader = null;
#endregion
//顯示信息----無連接關閉 .Accept()方法報錯?
public void AcceptMessage()
{
//接受客戶機的連接請求
clientSocket = socket.Accept(); //此處等待
if (clientSocket != null)
{
bConnected = true;
this.label1.Text = "與客戶 " + clientSocket.RemoteEndPoint.ToString() + " 成功建立連接。";
}
nStream = new NetworkStream(clientSocket);
//讀字節流
tReader = new StreamReader(nStream);
//寫字節流
wReader = new StreamWriter(nStream);
string sTemp; //臨時存儲讀取的字符串
while (bConnected)
{
try
{
//連續從當前流中讀取字符串直至結束
sTemp = tReader.ReadLine();
if (sTemp.Length != 0)
{
//richTextBox2_KeyPress()和AcceptMessage()
//都將向richTextBox1寫字符,可能訪問有衝突,
//所以,需要多線程互斥
lock (this)
{
richTextBox1.Text = "客戶機:" + sTemp + "\n" + richTextBox1.Text;
}
}
}
catch
{
tAcceptMsg.Abort();
MessageBox.Show("無法與客戶機通信。");
}
}
//禁止當前Socket上的發送與接收
clientSocket.Shutdown(SocketShutdown.Both);
//關閉Socket,並釋放所有關聯的資源
clientSocket.Close();
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
//啓動偵聽並顯示聊天信息--New Socket()
private void button1_Click(object sender, EventArgs e)
{
//服務器偵聽端口可預先指定(此處使用了最大端口值)
//Any表示服務器應偵聽所有網絡接口上的客戶活動
IPP = new IPEndPoint(IPAddress.Any, 45678);
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(IPP);//關聯(綁定)節點
socket.Listen(0);//0表示連接數量不限
//創建偵聽線程
tAcceptMsg = new Thread(new ThreadStart(this.AcceptMessage));
tAcceptMsg.Start();
button1.Enabled = false;
}
//發送信息
private void richTextBox2_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)13)//按下的是回車鍵
{
if (bConnected)
{
try
{
//richTextBox2_KeyPress()和AcceptMessage()
//都將向richTextBox1寫字符,可能訪問有衝突,
//所以,需要多線程互斥
lock (this)
{
richTextBox1.Text = "服務器:" + richTextBox2.Text + richTextBox1.Text;
//客戶機聊天信息寫入網絡流,以便服務器接收
wReader.WriteLine(richTextBox2.Text);
//清理當前緩衝區數據,使所有緩衝數據寫入基礎設備
wReader.Flush();
//發送成功後,清空輸入框並聚集之
richTextBox2.Text = "";
richTextBox2.Focus();
}
}
catch
{
MessageBox.Show("無法與客戶機通信!");
}
}
else
{
MessageBox.Show("未與客戶機建立連接,不能通信。");
}
}
}
//關閉窗體時斷開socket連接,並終止線程(否則,VS調試程序將仍處於運行狀態)
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
socket.Close();
tAcceptMsg.Abort();
}
catch
{ }
}
}
}
首先創建一個服務端Socket並明確端口號,通過accept()方法獲取到鏈接過來的客戶端Socket,從客戶端Socket中獲取輸入流,最後由輸入流讀取客戶端傳輸來的數據。
實驗結果:
五、TCP Socket多線程應用
TCP相比於UDP有一個缺點就是僅支持一對一連接,不可以一對多通信。但是通過多線程的技術手段,我們還是可以實現一對多通信的效果。
//僅需要修改AcceptMessage()方法即可
public void AcceptMessage()
{
//接受客戶機的連接請求
clientSocket = socket.Accept(); //此處等待
new Thread(() => {
if (clientSocket != null)
{
bConnected = true;
this.label1.Text = "與客戶 " + clientSocket.RemoteEndPoint.ToString() + " 成功建立連接。";
}
nStream = new NetworkStream(clientSocket);
//讀字節流
tReader = new StreamReader(nStream);
//寫字節流
wReader = new StreamWriter(nStream);
string sTemp; //臨時存儲讀取的字符串
while (bConnected)
{
try
{
//連續從當前流中讀取字符串直至結束
sTemp = tReader.ReadLine();
if (sTemp.Length != 0)
{
//richTextBox2_KeyPress()和AcceptMessage()
//都將向richTextBox1寫字符,可能訪問有衝突,
//所以,需要多線程互斥
lock (this)
{
richTextBox1.Text = "客戶機:" + sTemp + "\n" + richTextBox1.Text;
}
}
}
catch
{
tAcceptMsg.Abort();
MessageBox.Show("無法與客戶機通信。");
}
}
//禁止當前Socket上的發送與接收
clientSocket.Shutdown(SocketShutdown.Both);
//關閉Socket,並釋放所有關聯的資源
clientSocket.Close();
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
}
我這個程序還存在很多的BUG,需要學習的可自行完善。大概的思路就是這樣的。是應該新建一個Socket的而不是處理數據的線程。Server回覆給指定Socket的問題。。。。。。
六、Socket與HTTP
在計算機網絡中這兩者根本不是一個概念。Socket是一個封裝的API,基於TCP協議進行數據通信。而HTTP是超文本傳輸協議,是基於TCP的應用層協議。所以我們還可以通過Socket編程來實現以下HTTP協議的GET請求做一個測試。
這裏用ONENET服務器做測試,向ONENET服務器發送一下內容:
POST /devices/25900768/datapoints HTTP/1.1
api-key: WXs6QMIDeT1FSX1JwfVIFTRAh=o=
Host:api.heclouds.com
Connection:close
Content-Length:59
{"datastreams":[{"id":"111","datapoints":[{"value":50}]}]}
就可以在ONENET平臺上增加一個數據點了
七、總結
網絡編程也許大家可以做出來。但是如果大家不瞭解這些網絡協議還是希望大家學習一下。雖然對我們做網絡編程沒有多大影響,但是對於整個程序的深入理解會有很好的效果。
最後把所有程序打包給大家:https://mp.csdn.net/postedit?not_checkout=1