聊天程序简述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了。再次友情提醒一下,若是你不懂多线程,个人上一篇博客就是对他的浅解