作者:陈广
日期:2018-3-24
在上篇文章中,我们留下了很多问题。今天先解决服务无法侦听第二个连接的问题。想要解决这个问题,当然需要使用多线程了。这里我们先使用最原始的线程 Thread 来解决这问题。即使 Thread 已经被淘汰,但我觉得在这个场景下使用 Thread 还是挺好的,首先长线程不适合使用线程池,其次结构清晰,明了,易于理解,算是将来使用更高级异步网络编程的一个基础吧。
首先来看上篇文章中最后一个服务器程序的流程图:
由图可知s.Accept
会阻塞进程,一旦调用,服务器就无法做其它的事了。所以这里需要专门开一个线程用于s.Accept
。流程变成如下形式:
现在我们专门开了一个线程去执行s.Accept
并处理接收消息,服务器在等待连接的同时,可以去做其它的事情了。
新的问题来了,看上图可知程序一旦接收到连接,会继续往下执行,就再也无法再回到s.Accept
去接收新的连接了。而且recvSocket.Receive
也会阻塞程序,也就是说recvSocket.Receive
和s.Accept
是无法在同一个线程里同时工作的。这里就需要在接收到新客户端连接时,专门再开一个线程去处理这个客户端的事情。如下图所示:
现在终于可以同时处理多个连接请求了,而且每个请求互不干扰。下面按照这个想法把代码写出来。直接在上一篇文章的基础上修改代码。
添加以下命名空间:
using System.Threading;
更改代码如下:
static void Main(string[] args)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint point = new IPEndPoint(ip, 5000);
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.Bind(point);
s.Listen(5);
Console.WriteLine("服务器开始侦听...");
//创建线程去监听客户端连接
Thread listenThread = new Thread(() => ListenConnect(s));
listenThread.IsBackground = true; //一定要设置为后台线程,否则程序无法关闭
listenThread.Start();
Console.ReadLine();
}
//监听客户端连接的线程方法
static void ListenConnect(Socket s)
{
while (true)
{
Socket recvSocket = s.Accept();
Console.WriteLine("获取一个来自{0}的连接", recvSocket.RemoteEndPoint.ToString());
//创建专门的线程去监听客户端发送信息请求
Thread receiveThread = new Thread(() => ReceiveMessage(recvSocket));
receiveThread.IsBackground = true;
receiveThread.Start();
}
}
//接收指定客户端发送信息的线程
static void ReceiveMessage(Socket recvSocket)
{
string ip = recvSocket.RemoteEndPoint.ToString();
byte[] buff = new byte[1024]; //创建一个接收缓冲区
try
{
while (true)
{
int count = recvSocket.Receive(buff, buff.Length, SocketFlags.None);
string recvStr = Encoding.Unicode.GetString(buff, 0, count);
Console.WriteLine("接收到来自{0}数据:{1}", ip, recvStr);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
recvSocket.Close();//客户端关闭时会引发异常,此时关闭此连接
Console.WriteLine("{0} 已退出连接。", ip);
}
}
先运行服务器,然后找到上篇文章最后的客户端程序所生成的.exe
文件,打开多个客户端,运行效果如下:
我是先后打开三个客户端,然后提前关闭了第一个打开的客户端。
学了这么多,来练练手,做个控制台版聊天程序。当然,第一个聊天程序,一切重简,只允许一个服务器和一个客户端之间进行在聊天,任何一端即可发信息,也可收信息。
由于服务器只接收一个客户端连接,所以无需专门开一个线程专门侦听客户端连接。在侦听到连接之后,由于接收消息会阻塞程序运行,则需要开一个线程接收消息,开另一个线程发送消息。
修改服务器端代码如下:
static void Main(string[] args)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint point = new IPEndPoint(ip, 5000);
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.Bind(point);
s.Listen(5);
Console.WriteLine("服务器开始侦听...");
Socket subSocket = s.Accept();
Console.WriteLine("获取来自{0}的连接", subSocket.RemoteEndPoint.ToString());
//接收消息线程
Thread tRecv = new Thread(() => ReceiveMessage(subSocket));
tRecv.Start();
//发送消息线程
Thread tSend = new Thread(() => SendMessage(subSocket));
tSend.Start();
}
//接收消息的线程方法
static void ReceiveMessage(Socket recvSocket)
{
string ip = recvSocket.RemoteEndPoint.ToString();
byte[] buff = new byte[1024]; //创建一个接收缓冲区
try
{
while (true)
{
int count = recvSocket.Receive(buff, buff.Length, SocketFlags.None);
string recvStr = Encoding.Unicode.GetString(buff, 0, count);
Console.WriteLine("接收到来自{0}数据:{1}", ip, recvStr);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
recvSocket.Close();
Console.WriteLine("{0} 已退出连接。", ip);
}
}
//发送消息的线程方法
static void SendMessage(Socket subSocket)
{
while (true)
{
string sendStr = Console.ReadLine();
byte[] sendBuff = Encoding.Unicode.GetBytes(sendStr);
subSocket.Send(sendBuff, sendBuff.Length, SocketFlags.None);
}
}
这里需要注意的是线程不能设置为后台线程,否则程序会提前退出。现在的处理方法也不完善,仅作为演示。
客户端代码除了连接部分不一样,其余和服务器端代码类似。
修改客户端代码如下:
static void Main(string[] args)
{
//获取服务器端IP地址
IPAddress ip = IPAddress.Parse("127.0.0.1");
try
{
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.Connect(ip, 5000); //向服务器发起连接
Console.WriteLine("开始连接服务器 {0} ...", ip.ToString());
if(s.Connected)
{
Console.WriteLine("连接成功!");
}
//发送消息线程
Thread tSend = new Thread(() => SendMessage(s));
tSend.Start();
//接收消息线程
Thread tRecv = new Thread(() => ReceiveMessage(s));
tRecv.Start();
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
//发送消息线程方法
static void SendMessage(Socket s)
{
while (true)
{
string sendStr = Console.ReadLine();
byte[] sendBuff = Encoding.Unicode.GetBytes(sendStr);
s.Send(sendBuff, sendBuff.Length, SocketFlags.None);
}
}
//接收消息线程方法
static void ReceiveMessage(Socket s)
{
byte[] recvBuff = new byte[1024];
try
{
while (true)
{
int count = s.Receive(recvBuff, recvBuff.Length, SocketFlags.None);
string recvStr = Encoding.Unicode.GetString(recvBuff, 0, count);
Console.WriteLine("收到服务器信息:{0}", recvStr);
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
s.Close();
Console.WriteLine("服务器已经退出连接。");
}
}
先运行服务器代码,再运行客户端代码,效果如下:
控制台版聊天程序界面非常不友好,用起来不是那么地爽,这个程序只是让大家能够了解聊天程序的大概流程而已。接下来我们做一个Windows应用程序版本的,当然,也只接受一对一聊天,简单为主。
先做服务器端,新建一个Windows应用程序项目,拖放如下图所的控件:
下表是各控件属性设置:
控件类型 | 控件命名 | 需设置的属性 |
---|---|---|
TextBox | txtRecv | Multiline:True ScrollBars:Vertial |
TextBox | txtSend | |
Button | btnSend | Text:发送 |
引用如下命名空间:
using System;
using System.Net;
using System.Net.Sockets;
using System.Windows.Forms;
using System.Threading;
using System.Text;
双击【发送】按钮生成按钮事件;生成txtSend
的KeyDown
事件;生成窗体的Load
事件。最终代码如下:
Socket serverSocket;
Socket subSocket;
private void Form1_Load(object sender, EventArgs e)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint point = new IPEndPoint(ip, 5000);
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(point);
serverSocket.Listen(5);
txtRecv.Text = "开始侦听...";
Thread tListen = new Thread(Listening);
tListen.IsBackground = true;
tListen.Start();
}
private void btnSend_Click(object sender, EventArgs e)
{
if (txtSend.Text != "")
{
byte[] sendBuff = Encoding.Unicode.GetBytes(txtSend.Text);
subSocket.Send(sendBuff, sendBuff.Length, SocketFlags.None);
txtSend.Clear(); //清除发送文本框
}
}
private void txtSend_KeyDown(object sender, KeyEventArgs e)
{ //监控文本框键盘事件,如果按下回车键,则发送消息
if (e.KeyCode == Keys.Enter)
{
btnSend_Click(null, null);
}
}
private void Listening()
{
subSocket = serverSocket.Accept();
this.Invoke(new Action(() =>
{
btnSend.Enabled = true;
txtRecv.Text += "\r\n获取来自" + subSocket.RemoteEndPoint.ToString() + "的连接";
}));
byte[] buff = new byte[1024];
try
{
while (true)
{
int count = subSocket.Receive(buff, buff.Length, SocketFlags.None);
string recvStr = Encoding.Unicode.GetString(buff, 0, count);
this.Invoke(new Action(() => txtRecv.Text += "\r\n" + recvStr));
}
}
catch (Exception e)
{
this.Invoke(new Action(() => txtRecv.Text += "\r\n" + e.Message));
}
finally
{
subSocket.Close();
}
}
由于只接受一个客户端连接,这里只开了一个线程用于监听连接和等待接收消息。其实发送也应该做成异步的,因为当发送出问题时也会阻塞程序,只是出问题的几率不大,所以这里省事直接使用同步代码。
界面如下图所示:
下表是各控件属性设置:
控件类型 | 控件命名 | 需设置的属性 |
---|---|---|
TextBox | txtIP | text:127.0.0.1 |
TextBox | txtPort | text:5000 |
Button | btnConnect | |
TextBox | txtRecv | Multiline:True ScrollBars:Vertial |
TextBox | txtSend | Text:连接服务器 |
Button | btnSend | Text:发送 |
代码如下:
Socket clientSocket;
private void btnConnect_Click(object sender, EventArgs e)
{
IPAddress ip = IPAddress.Parse(txtIP.Text);
int port = int.Parse(txtPort.Text);
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect(ip, port);
try
{
if (clientSocket.Connected)
{
txtRecv.Text = "连接服务器 " + txtIP.Text + " 成功";
btnConnect.Enabled = false;
btnSend.Enabled = true;
}
Thread tRecv = new Thread(ReceiveMessage);
tRecv.IsBackground = true;
tRecv.Start();
}
catch (Exception ex)
{
txtRecv.Text += "\r\n" + ex.Message;
}
}
private void btnSend_Click(object sender, EventArgs e)
{
if (txtSend.Text != "")
{
byte[] sendBuff = Encoding.Unicode.GetBytes(txtSend.Text);
clientSocket.Send(sendBuff, sendBuff.Length, SocketFlags.None);
txtSend.Clear(); //清除发送文本框
}
}
private void txtSend_KeyDown(object sender, KeyEventArgs e)
{
//监控文本框键盘事件,如果按下回车键,则发送消息
if (e.KeyCode == Keys.Enter)
{
btnSend_Click(null, null);
}
}
private void ReceiveMessage()
{
byte[] buff = new byte[1024];
try
{
while (true)
{
int count = clientSocket.Receive(buff, buff.Length, SocketFlags.None);
string recvStr = Encoding.Unicode.GetString(buff, 0, count);
this.Invoke(new Action(() => txtRecv.Text += "\r\n" + recvStr));
}
}
catch (Exception e)
{
this.Invoke(new Action(() =>
{
txtRecv.Text += "\r\n" + e.Message;
btnConnect.Enabled = true;
btnSend.Enabled = false;
}));
}
finally
{
clientSocket.Close();
}
}
代码和服务端类似,只是用了一个接收消息线程,发送消息同步。
运行效果如下图所示: