聊天程序簡述html
1、目的:主要是爲了闡述Socket,以及應用多線程,本文側重Socket相關網路編程的闡述。若是您對多線程不瞭解,你們能夠看下個人上一篇博文淺解多線程 。程序員
2、功能:此聊天程序功能實現了服務端跟多個客戶端之間的聊天,能夠羣發消息,選擇ip發消息,客戶端向服務端發送文件。 (例子爲WinForm應用程序)編程
Socket,端口,Tcp,UDP。 概念設計模式
1、Socket還被稱做「套接字」,應用程序一般經過套接字向網絡發送請求或者應答網絡請求。根據鏈接啓動的方式以及本地套接字要鏈接的目標,套接字之間的鏈接過程能夠分爲三個步驟:服務器監聽,客戶端請求,鏈接確認。數組
2、端口:能夠認爲是計算機與外界通信交流的出口。
服務器
3、Tcp: TCP是一種面向鏈接(鏈接導向)的、可靠的、基於字節流的運輸層通訊協議。UDP是另外一個重要的傳輸協議。
網絡
4、UDP:用戶數據報協議,是一種無鏈接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。多線程
理解Socket,端口,Tcp,UDPsocket
1、ip跟端口的做用:例如,你用QQ跟好友聊天,首先QQ要知根據好友所在電腦的IP地址發送信息,ip地址能肯定好友的所在的電腦,可是不知道好友電腦上的QQ應用程序是哪個,這就須要QQ提供一個端口號來肯定你發過來的信息是QQ接受的數據。這樣就簡單的闡述了Ip跟端口的做用。
函數
2、Tcp,Udp做用以及差別:首先要說的是,這是兩種網路協議,他們的差異就是TCP協議中包含了專門的傳遞保證機制,當數據接收方收到發送方傳來的信息時,會自動向發送方發出確認消息;發送方只有在接收到該確認消息以後才繼續傳送其它信息,不然將一直等待直到收到確認信息爲止。與TCP不一樣,UDP協議並不提供數據傳送的保證機制。若是在從發送方到接收方的傳遞過程當中出現數據報的丟失,協議自己並不能作出任何檢測或提示。咱們.net程序員通常的應用程序用的都是Tcp協議。可是Tcp協議的執行速度,效率不及Udp快。看別人的博客感受圖解這兩個協議,顯得更直觀點。上圖:
3、Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。出自同一篇博客的圖。
4、到這裏若是你對Socket,還不是很清楚透徹,那麼在接下來的聊天程序代碼中,我還會一點點的闡述。
建立服務端監聽功能———聊天程序(Socket、Thread)
服務端監聽服務是建立一個Socket等待接收客戶端的信息。這個須要綁定服務端的Ip、端口號,以便於客戶端發送請求的時候找準確服務端聊天程序的具體位置。此外這個Socket還須要設置監聽序列的大小,告知應用程序一次性最多處理客戶端發來信息的多少。而後建立一個接收客戶端通訊的Socket,等待客戶段發來的信息。
Socket sck = null;
//點擊開啓服務端監聽
private void btn_StarServer_Click(object sender, EventArgs e)
{
//建立一個Socket實例
//第一個參數表示使用ipv4
//第二個參數表示發送的是數據流
//第三個參數表示使用的協議是Tcp協議
sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//獲取ip地址
IPAddress ip = IPAddress.Parse(txt_ip.Text);
//建立一個網絡通訊節點,這個通訊節點包含了ip地址,跟端口號。
//這裏的端口咱們設置爲1029,這裏設置大於1024,爲何本身查一下端口號範圍使用說明。
IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));
//Socket綁定網路通訊節點
sck.Bind(endPoint);
//設置監聽隊列
sck.Listen(10);
ShowMsg("開啓監聽!");
//建立一個接收客戶端通訊的Socket
Socket accSck = sck.Accept();
//若是監聽到客戶端有連接,則運行到下一部,提示,連接成功!
ShowMsg("連接成功!");
}
//消息框裏面數據
void ShowMsg(string str)
{
string Ystr="";
if (txt_AccMsg.Text != "")
{
Ystr = txt_AccMsg.Text + "\r\n";
}
txt_AccMsg.Text = Ystr+str;
}
問題1:代碼中的Socket accSck = sck.Accept();這個Socket是讓上一個綁定服務端ip端口號的Socket一直處於等待接受客戶端發送信息的狀態,因此一直佔用應用程序一直默認開啓的Ui線程,導致點擊開啓服務監聽後,界面無響應。
解決辦法:使用多線程,咱們在這裏寫一個本身的線程讓這裏的監聽服務,寫在本身的線程裏面。修改代碼以下:
Socket sck = null;
Thread thread = null;
//點擊開啓服務端監聽
private void btn_StarServer_Click(object sender, EventArgs e)
{
//建立一個Socket實例
//第一個參數表示使用ipv4
//第二個參數表示發送的是數據流
//第三個參數表示使用的協議是Tcp協議
sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//獲取ip地址
IPAddress ip = IPAddress.Parse(txt_ip.Text);
//建立一個網絡通訊節點,這個通訊節點包含了ip地址,跟端口號。
//這裏的端口咱們設置爲1029,這裏設置大於1024,爲何本身查一下端口號範圍使用說明。
IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));
//Socket綁定網路通訊節點
sck.Bind(endPoint);
//設置監聽隊列
sck.Listen(10);
ShowMsg("開啓監聽!");
//開啓一個線程,放入Socket服務監聽,上一篇博文中沒有介紹這樣的線程實例化方法。這裏特別說下這樣是能夠的。
Thread thread = new Thread(JtSocket);
//設置爲後臺線程
thread.IsBackground = true;
thread.Start();
}
//Socket服務監聽函數
void JtSocket()
{
//建立一個接收客戶端通訊的Socket
Socket accSck = sck.Accept();
//若是監聽到客戶端有連接,則運行到下一部,提示,連接成功!
ShowMsg("連接成功!");
}
問題2:代碼中sck.Listen(10);設置監聽序列,這裏設置爲10是否是,服務端只能處理10個客戶段的請求呢。
答:不是的這裏設置的是一次性只能處理10個,若是還有更多就在後面排隊,等待這10個處理完成,接下來在處理排着對的信息。
開啓服務監聽看一下咱們的聊天界面:
而後咱們再作一個客戶端,連接到服務端。
建立客戶端連接服務端的Socket———聊天程序(Socket、Thread)
若是連接服務端的聊天程序則須要知道服務端的Ip地址,端口號。
Socket clientSocket = null;
Thread thread = null;
//經過IP地址與端口號與服務端創建連接
private void btn_ConServer_Click(object sender, EventArgs e)
{
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//這裏的ip地址,端口號都是服務端綁定的相關數據。
IPAddress ip = IPAddress.Parse(txt_Clientip.Text);
IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));
clientSocket.Connect(endpoint);//連接有端口號與IP地址肯定服務端.
}
而後點擊鏈接服務,查看咱們的聊天界面。(首先先打開服務端應用程序,點擊開啓監聽,而後打開客戶端應用程序,點擊連接服務)
連接成功後,下一步,咱們就開始咱們的聊天信息接收發送了。
服務端向客戶端發送信息,客戶端接受信息———聊天程序(Socket、Thread)
1、這裏咱們發送消息是經過Tcp協議以 字節數組的類型形式發送,因此在發送以前咱們須要把要發送,接收的數據作一個轉換爲字節數組的類型。
2、客戶端經過建立的連接服務端的Socket的Receive方法接收消息,服務端經過建立的接受客戶端信息的Socket的Send方法發送消息。
服務端代碼:
Socket sck = null;
Thread thread = null;
//點擊開啓服務端監聽
private void btn_StarServer_Click(object sender, EventArgs e)
{
//建立一個Socket實例
//第一個參數表示使用ipv4
//第二個參數表示發送的是數據流
//第三個參數表示使用的協議是Tcp協議
sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//獲取ip地址
IPAddress ip = IPAddress.Parse(txt_ip.Text);
//建立一個網絡通訊節點,這個通訊節點包含了ip地址,跟端口號。
//這裏的端口咱們設置爲1029,這裏設置大於1024,爲何本身查一下端口號範圍使用說明。
IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));
//Socket綁定網路通訊節點
sck.Bind(endPoint);
//設置監聽隊列
sck.Listen(10);
ShowMsg("開啓監聽!");
//開啓一個線程,放入Socket服務監聽,上一篇博文中沒有介紹這樣的線程實例化方法。這裏特別說下這樣是能夠的。
Thread thread = new Thread(JtSocket);
//設置爲後臺線程
thread.IsBackground = true;
thread.Start();
}
Socket accSck = null;
//Socket服務監聽函數
void JtSocket()
{
while (true)//注意該循環,服務端要持續監聽,要否則一個客戶端連接事後就沒法連接第二個客戶端了。
{
//建立一個接收客戶端通訊的Socket
accSck = sck.Accept();
//若是監聽到客戶端有連接,則運行到下一部,提示,連接成功!
ShowMsg("連接成功!");
}
}
//消息框裏面數據
void ShowMsg(string str)
{
string Ystr="";
if (txt_AccMsg.Text != "")
{
Ystr = txt_AccMsg.Text + "\r\n";
}
txt_AccMsg.Text = Ystr+str;
}
//向客戶端發送數據
private void btn_SendSingleMsg_Click(object sender, EventArgs e)
{
string SendMsg = txt_SendMsg.Text;
if (SendMsg != "")
{
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(SendMsg); //將要發送的數據,生成字節數組。
accSck.Send(buffer);
ShowMsg("向客戶端發送了:" + SendMsg);
}
}
客戶端代碼:
public Form1()
{
InitializeComponent();
TextBox.CheckForIllegalCrossThreadCalls = false;
}
Socket clientSocket = null;
Thread thread = null;
//經過IP地址與端口號與服務端創建連接
private void btn_ConServer_Click(object sender, EventArgs e)
{
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//這裏的ip地址,端口號都是服務端綁定的相關數據。
IPAddress ip = IPAddress.Parse(txt_Clientip.Text);
IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));
clientSocket.Connect(endpoint);//連接有端口號與IP地址肯定服務端.
//客戶端在接受服務端發送過來的數據是經過Socket 中的Receive方法,
//該方法會阻斷線程,因此咱們本身爲該方法建立了一個線程
thread = new Thread(ReceMsg);
thread.IsBackground = true;//設置後臺線程
thread.Start();
}
//接收服務端數據
public void ReceMsg()
{
while (true)
{
byte[] buffer = new byte[1024 * 1024 * 2];
clientSocket.Receive(buffer);//接收服務端發送過來的數據
string ReceiveMsg = System.Text.Encoding.UTF8.GetString(buffer);//把接收到的字節數組轉成字符串顯示在文本框中。
ShowMsg("接收到數據:" + ReceiveMsg);
}
}
//消息框裏面數據
void ShowMsg(string str)
{
string Ystr = "";
if (txt_ClientMsg.Text != "")
{
Ystr = txt_ClientMsg.Text + "\r\n";
}
txt_ClientMsg.Text = Ystr + str;
}
啓動服務端應用程序,點擊啓動服務監聽,啓動客戶端應用程序,點擊鏈接服務,而後在消息框內輸入消息,點擊發送。運行效果以下。
接下來作客戶端向服務端發送消息:
客戶端向服務端發送信息(文件,字符串),客戶端接受信息———聊天程序(Socket、Thread)
1、這裏咱們發送不只只有字符串還有文件。他們都是一字節數組的類型發送出去,區別字符串和文件的思想是:把字節數組的第一個值設置爲0跟1,用來區分。
2、這裏發送的文件接受的時候,重命名,還要爲他寫上後綴名。沒有深刻寫。
3、這裏客戶端鏈接服務端的成功後,把客戶端的ip端口號,寫入list列表中,同時也存入Dictionary<string, Socket> socketDir集合中,便於服務端與多個客戶端鏈接時,選擇發送信息。同時也避免了,不知道發送給哪一個客戶端數據。
客戶端代碼:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;
namespace CharClient
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
TextBox.CheckForIllegalCrossThreadCalls = false;
}
Socket clientSocket = null;
Thread thread = null;
//經過IP地址與端口號與服務端創建連接
private void btn_ConServer_Click(object sender, EventArgs e)
{
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//這裏的ip地址,端口號都是服務端綁定的相關數據。
IPAddress ip = IPAddress.Parse(txt_Clientip.Text);
IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));
clientSocket.Connect(endpoint);//連接有端口號與IP地址肯定服務端.
//客戶端在接受服務端發送過來的數據是經過Socket 中的Receive方法,
//該方法會阻斷線程,因此咱們本身爲該方法建立了一個線程
thread = new Thread(ReceMsg);
thread.IsBackground = true;//設置後臺線程
thread.Start();
}
//接收服務端數據
public void ReceMsg()
{
while (true)
{
byte[] buffer = new byte[1024 * 1024 * 2];
clientSocket.Receive(buffer);//接收服務端發送過來的數據
string ReceiveMsg = System.Text.Encoding.UTF8.GetString(buffer);//把接收到的字節數組轉成字符串顯示在文本框中。
ShowMsg("接收到數據:" + ReceiveMsg);
}
}
//消息框裏面數據
void ShowMsg(string str)
{
string Ystr = "";
if (txt_ClientMsg.Text != "")
{
Ystr = txt_ClientMsg.Text + "\r\n";
}
txt_ClientMsg.Text = Ystr + str;
}
//向服務端發送消息
private void btn_ClientSendSingleMsg_Click(object sender, EventArgs e)
{
string txtMsg = txt_ClientSendMsg.Text;
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(txtMsg);
byte[] newbuffer = new byte[buffer.Length + 1];//定義一個新數組
newbuffer[0] = 0;//設置標識,表示發送的是字符串
Buffer.BlockCopy(buffer, 0, newbuffer, 1, buffer.Length);//源數組中的數據拷貝到新數組中
clientSocket.Send(newbuffer);//發送新數組中的數據
}
//向服務端發送文件
private void btn_ClientSendfile_Click(object sender, EventArgs e)
{
using (FileStream fs = new FileStream(txt_ClientFile.Text, FileMode.Open))
{
byte[] buffer = new byte[1024 * 1024 * 2];
int readLength = fs.Read(buffer, 0, buffer.Length);
byte[] newbuffer = new byte[readLength + 1];//定義一個新的數組,而後將原有數組中的數據拷貝該數組中。
newbuffer[0] = 1;//將第一單元設置爲1,表示傳送的是文件.
//將數據有一個數組拷貝到另外一個數組.
//第一參數:表示源數組
//第二個:表示從源數組中的哪一個位置開始拷貝
//第三個:表示目標數組。
//第四個:表示從目標數組的哪一個位置開始填充.
//五:表示:拷貝多少數據
Buffer.BlockCopy(buffer, 0, newbuffer, 1, readLength);
clientSocket.Send(newbuffer);
}
}
//打開文件夾,選擇要發送的文件
private void Btn_see_Click(object sender, EventArgs e)
{
OpenFileDialog openfile = new OpenFileDialog();
if (openfile.ShowDialog() == DialogResult.OK)
{
txt_ClientFile.Text = openfile.FileName;
}
}
}
}
服務端代碼:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;
namespace ChatServer
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
TextBox.CheckForIllegalCrossThreadCalls = false;
}
Socket sck = null;
Thread thread = null;
//點擊開啓服務端監聽
private void btn_StarServer_Click(object sender, EventArgs e)
{
//建立一個Socket實例
//第一個參數表示使用ipv4
//第二個參數表示發送的是數據流
//第三個參數表示使用的協議是Tcp協議
sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//獲取ip地址
IPAddress ip = IPAddress.Parse(txt_ip.Text);
//建立一個網絡通訊節點,這個通訊節點包含了ip地址,跟端口號。
//這裏的端口咱們設置爲1029,這裏設置大於1024,爲何本身查一下端口號範圍使用說明。
IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_port.Text));//建立一個網絡通訊節點,該節點中包含了IP地址和端口號.
//Socket綁定網路通訊節點
sck.Bind(endpoint);
//設置監聽隊列
sck.Listen(10);
ShowMsg("開啓監聽!");
//開啓一個線程,放入Socket服務監聽,上一篇博文中沒有介紹這樣的線程實例化方法。這裏特別說下這樣是能夠的。
Thread thread = new Thread(ConnectAccept);
//設置爲後臺線程
thread.IsBackground = true;
thread.Start();
}
//消息框裏面數據
void ShowMsg(string str)
{
string Ystr="";
if (txt_AccMsg.Text != "")
{
Ystr = txt_AccMsg.Text + "\r\n";
}
txt_AccMsg.Text = Ystr+str;
}
//向客戶端發送數據
private void btn_SendSingleMsg_Click(object sender, EventArgs e)
{
string sendMsg = this.txt_SendMsg.Text;//獲取要發送到客戶端的文本
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sendMsg);//生成字節數組
if (!string.IsNullOrEmpty(this.lsb_Ips.Text))
{
string ipendpoint = this.lsb_Ips.Text;//在服務端,選擇與客戶端進行通訊的IP地址與端口號
socketDir[ipendpoint].Send(buffer);//向客戶端發送數據
ShowMsg("向客戶端發送了:" + sendMsg);
}
else
{
MessageBox.Show("請選擇與哪一個客戶端進行通訊");
}
}
// Socket newSoket = null;//.:不能將與客戶端進行通訊的Socket定義成全局的.
Dictionary<string, Socket> socketDir = new Dictionary<string, Socket>();//將每個與客戶端進行通訊的Socket放到該集合中.
public void ConnectAccept()
{
while (true)//注意該循環,服務端要持續監聽
{
Socket newSoket = sck.Accept();//接收客戶端發過來的數據,而且建立了一個新的Socket實例.
socketDir.Add(newSoket.RemoteEndPoint.ToString(), newSoket);//將負責與客戶端進行通訊的Socket實例添加到集合中。
lsb_Ips.Items.Add(newSoket.RemoteEndPoint.ToString());
ShowMsg("客戶端連接成功!");
ParameterizedThreadStart par = new
ParameterizedThreadStart(RecevieMsg);
Thread thread = new Thread(par);//因爲服務端接收客戶端發送過來的數據是經過Recevie方法,該方法會阻斷線程,因此咱們從新定義一個針對該方法的線程.
// thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start(newSoket);//注意:不要忘記傳遞socket實例
}
}
//該方法負責接收從客戶端發送過來的數據
public void RecevieMsg(object socket)
{
Socket newSocket = socket as Socket;//轉成對應的Socket類型
while (true)
{
byte[] buffer = new byte[1024 * 1024 * 2];
int receiveLength = -1;
try //因爲Socket中的Receive方法容易拋出異常,因此咱們在這裏要捕獲異常。
{
receiveLength = newSocket.Receive(buffer);//接收從客戶端發送過來的數據
}
catch (SocketException ex)//注意:在捕獲異常時,先肯定具體的異常類型。
{
ShowMsg("出現了異常:" + ex.Message);
socketDir.Remove(newSocket.RemoteEndPoint.ToString());//若是出現了異常,將該Socket實例從集合中移除
lsb_Ips.Items.Remove(newSocket.RemoteEndPoint.ToString());
break;//出現異常之後,終止整個循環的執行
}
catch (Exception ex)
{
ShowMsg("出現了異常:" + ex.Message);
break;
}
if (buffer[0] == 0)//表示字符串
{
string str = System.Text.Encoding.UTF8.GetString(buffer, 1, receiveLength - 1);//注意,是從下標爲1的開始轉成字符串,爲0的是標識。
ShowMsg(str);
}
else if (buffer[0] == 1)//表示文件
{
SaveFileDialog savafile = new SaveFileDialog();
if (savafile.ShowDialog() == DialogResult.OK)
{
using (FileStream fs = new FileStream(savafile.FileName, FileMode.Create))
{
fs.Write(buffer, 1, receiveLength - 1);//將文件寫到磁盤上,從1開始到receiveLength-1
ShowMsg("文件寫成功!");
}
}
}
}
}
}
}
啓動服務端應用程序,點擊啓動服務監聽,能夠同時啓動多個客戶端應用程序,都要先點擊鏈接服務,而後在消息框內輸入消息,也能夠選取文件,點擊發送。運行效果以下。
總結:剩餘一個羣發,我沒寫上去,相信你若是看明白了上面我所寫的的話,這個羣發,就so easy了。再次友情提醒一下,若是你不懂多線程,個人上一篇博客就是對他的淺解