C#基于Socket的简单聊天室实践

序:实现一个基于Socket的简易的聊天室,实现的思路如下:

程序的结构:多个客户端+一个服务端,客户端都是向服务端发送消息,然后服务端转发给所有的客户端,这样形成一个简单的聊天室功能。

实现的细节:服务端启动一个监听套接字。每一个客户端连接到服务端,都是开启了一个线程,线程函数是封装了通信套接字,来实现与客户端的通信。多个客户端连接时产生的通信套接字用一个静态的Dictionary保存。具体的实现可以参考代码及其注释。

术语理解:

套接字Socket:源于Unix,为了解决传输层网络编程的问题,Unix提供了类似于文件操作的方式来完成网络编程。要实现不同的主机,不同的程序之间进行通信,必须有相应的协议,这个协议便是TCP/IP协议。Socket是负责传输层的,基于TCP的。不同的主机之间可以通过IP地址识别,但是不同的主机上有众多的程序或者说是进程。一台主机上的进程要跟另一台主机上的进程通信,必须有双方能够唯一识别的标志。就像人的身份证号,手机号等。这里出现了EndPoint(端点)的概念。

EndPoint(端点):由IP地址和端口号构成,端口对应进程。这两个组合起来可以唯一的标识网络中某台主机上的某一个进程。这样就有一个唯一的身份标识,后面可以进行通信了。

每一个Socket需要绑定到端点进行通信。

Socket的常见的通信数据类型有两种:数据报(SOCK_DGRAM)和数据流(SOCK_STREAM),使用的网络协议TCP或UDP等等。

关于TCP

TCP是一种面向连接的,可靠的,基于字节流的传输层通信协议。

TCP的工作过程包括三个方面:

(1)建立连接:这个过程称为三次握手。

第一次:客户端发送SYN包(SEQ=x)到服务器,并进入SYN_SEND状态,等待服务器确认。

第二次:服务器收到SYN包,必须确认客户端的SYN(ACK=x+1),同时自己发送一个SYN包(SEQ=y),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次:客户端收到服务器发来的SYN+ACK包,向服务器发送确认包ACK(ACK=y+1),此包发送完毕,客户端和服务端进入Established状态。至此三次握手完成。

(2)传输数据:一旦通信双方建立了TCP连接,就可以相互发送数据。

(3)终止连接:关闭连接,需要四次握手,这个是由于TCP的半关闭造成的。

这个网上有很多资料,大家可以查阅下。

关于.NET里面的Socket

在.NET里面的System.Net.Sockets命名空间下提供了对Socket的操作。并且专门封装了TcpClient和TcpListener两个类来简化操作。我这里是直接用Socket实现的。这里分为这样几个步骤:

在服务端:

(1)声明一个套接字(称为监听套接字)Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

(2)声明一个端点(EndPoint)上面提到过Socket需要跟它绑定才能通信。IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);

(3)设置监听队列serverSocket.Listen(100);

(4)通过Accept()方法来获取一个通信套接字(当有客户端连接时),这个方法会阻塞线程,避免界面卡死的现象,启动一个线程,把这个Accept()放在线程函数里面。

在客户端:

(1)声明一个套接字,通过connect()向服务器发起连接。

(2)通过Receive方法获取服务器发来的消息(这里同样启用一个线程,通过while循环来实时监听服务器端发送的消息)

注意:数据是以字节流(Byte[])的形式传递的,我会使用Encoding.UTF8.GetString()方法来获取为字符串。都是通过Send()来向彼此发送消息。

聊天室开始

我这里使用的是WPF来创建的Server和Client端。

首先看下服务端,我创建一个ChatServer的WPF项目,包含一个ChatServer.xaml:

后台代码:

namespace ChatWPFServer
{
 /// <summary>
 /// Interaction logic for MainWindow.xaml
 /// </summary>
 public partial class ChatServer : Window
 {

 public ChatServer()
 {
 InitializeComponent();
 }
 //保存多个客户端的通信套接字
 public static Dictionary<String, Socket> clientList = null;
 //声明一个监听套接字
 Socket serverSocket = null;
 //设置一个监听标记
 Boolean isListen = true;
 private void btnStart_Click(object sender, RoutedEventArgs e)
 {
 if (serverSocket == null)
 {
 try
 {
 clientList = new Dictionary<string, Socket>();
 serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//实例监听套接字
 IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);//端点
 serverSocket.Bind(endPoint);//绑定
 serverSocket.Listen(100);//设置最大连接数
 Thread th = new Thread(StartListen);
 th.IsBackground = true;
 th.Start();
 txtMsg.Dispatcher.BeginInvoke(new Action(() =>
 {
 txtMsg.Text += "服务启动...\r\n";
 }));
 }
 catch (SocketException ex)
 {
 MessageBox.Show(ex.ToString());
 }

 }
 }

 //线程函数,封装一个建立连接的通信套接字
 private void StartListen()
 {
 isListen = true;
 Socket clientSocket = default(Socket);

 while (isListen)
 {
 try
 {
 clientSocket = serverSocket.Accept();//这个方法返回一个通信套接字
 }
 catch (SocketException ex)
 {
 File.AppendAllText("E:\\Exception.txt", ex.ToString() + "\r\nStartListen\r\n" + DateTime.Now.ToString() + "\r\n");
 }

 Byte[] bytesFrom = new Byte[4096];
 String dataFromClient = null;
 if (clientSocket != null && clientSocket.Connected)
 {
 try
 {
 Int32 len = clientSocket.Receive(bytesFrom);//获取客户端发来的信息
 if (len > -1)
 {
 String tmp = Encoding.UTF8.GetString(bytesFrom, 0, len);
 try
 {
 dataFromClient = EncryptionAndDecryption.TripleDESDecrypting(tmp);
 }
 catch (Exception ex)
 {

 }

 Int32 sublen = dataFromClient.LastIndexOf("$");
 if (sublen > -1)
 {
 dataFromClient = dataFromClient.Substring(0, sublen);
 if (!clientList.ContainsKey(dataFromClient))
 {
 clientList.Add(dataFromClient, clientSocket);

 BroadCast.PushMessage(dataFromClient + " Joined ", dataFromClient, false, clientList);

 HandleClient client = new HandleClient();

 client.StartClient(clientSocket, dataFromClient, clientList);

 }
 else
 {
 clientSocket.Send(Encoding.UTF8.GetBytes(EncryptionAndDecryption.TripleDESEncrypting("#" + dataFromClient + "#")));
 }
 }
 }
 }
 catch (Exception ex)
 {
 File.AppendAllText("E:\\Exception.txt", ex.ToString() + "\r\n\t\t" + DateTime.Now.ToString() + "\r\n");
 }

 }
 }


 }

 private void btnStop_Click(object sender, RoutedEventArgs e)
 {
 if (serverSocket != null)
 {

 foreach (var socket in clientList.Values)
 {
 socket.Close();
 }
 clientList.Clear();

 serverSocket.Close();
 serverSocket = null;
 isListen = false;
 txtMsg.Text += "服务停止\r\n";
 }
 }

 private void Window_Closed(object sender, EventArgs e)
 {
 isListen = false;
 BroadCast.PushMessage("Server has closed", "", false, clientList);
 clientList.Clear();
 serverSocket.Close();
 serverSocket = null;
 }

 private void Window_Loaded(object sender, RoutedEventArgs e)
 {
 try
 {
 clientList = new Dictionary<string, Socket>();
 serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//实例监听套接字
 IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);//端点
 serverSocket.Bind(endPoint);//绑定
 serverSocket.Listen(100);//设置最大连接数
 Thread th = new Thread(StartListen);
 th.IsBackground = true;
 th.Start();
 txtMsg.Dispatcher.BeginInvoke(new Action(() =>
 {
 txtMsg.Text += "服务启动...\r\n";
 }));
 }
 catch (SocketException ex)
 {
 MessageBox.Show(ex.ToString());
 }
 }

 }

 //这里专门负责接收客户端发来的消息,并且转发给所有的客户端
 public class HandleClient
 {
 Socket clientSocket;
 String clNo;
 Dictionary<String, Socket> clientList = new Dictionary<string, Socket>();
 public void StartClient(Socket inClientSocket, String clientNo, Dictionary<String, Socket> cList)
 {
 clientSocket = inClientSocket;
 clNo = clientNo;
 clientList = cList;
 Thread th = new Thread(Chat);
 th.IsBackground = true;
 th.Start();
 }

 private void Chat()
 {
 Byte[] bytesFromClient = new Byte[4096];
 String dataFromClient;
 String msgTemp = null;
 Byte[] bytesSend = new Byte[4096];
 Boolean isListen = true;

 while (isListen)
 {
 try
 {
 Int32 len = clientSocket.Receive(bytesFromClient);
 if (len > -1)
 {
 dataFromClient = EncryptionAndDecryption.TripleDESDecrypting(Encoding.UTF8.GetString(bytesFromClient, 0, len));
 if (!String.IsNullOrWhiteSpace(dataFromClient))
 {
 dataFromClient = dataFromClient.Substring(0, dataFromClient.LastIndexOf("$"));
 if (!String.IsNullOrWhiteSpace(dataFromClient))
 {
 BroadCast.PushMessage(dataFromClient, clNo, true, clientList);
 msgTemp = clNo + ": " + dataFromClient + "\t\t" + DateTime.Now.ToString();
 String newMsg = msgTemp;
 File.AppendAllText("E:\\MessageRecords.txt", newMsg + "\r\n", Encoding.UTF8);
 }
 else
 {
 isListen = false;
 clientList.Remove(clNo);
 clientSocket.Close();
 clientSocket = null;
 }

 }
 }
 }
 catch (Exception ex)
 {
 isListen = false;
 clientList.Remove(clNo);
 clientSocket.Close();
 clientSocket = null;
 File.AppendAllText("E:\\Exception.txt", ex.ToString() + "\r\nChat\r\n" + DateTime.Now.ToString() + "\r\n");
 }
 }
 }
 }

 //向所有的客户端发送消息
 public class BroadCast
 {
 public static void PushMessage(String msg, String uName, Boolean flag, Dictionary<String, Socket> clientList)
 {
 foreach (var item in clientList)
 {
 Socket brdcastSocket = (Socket)item.Value;
 String msgTemp = null;
 Byte[] castBytes = new Byte[4096];
 if (flag == true)
 {
 msgTemp = EncryptionAndDecryption.TripleDESEncrypting(uName + ": " + msg + "\t\t" + DateTime.Now.ToString());
 castBytes = Encoding.UTF8.GetBytes(msgTemp);
 }
 else
 {
 msgTemp = EncryptionAndDecryption.TripleDESEncrypting(msg + "\t\t" + DateTime.Now.ToString());
 castBytes = Encoding.UTF8.GetBytes(msgTemp);
 }
 try
 {
 brdcastSocket.Send(castBytes);
 }
 catch (Exception ex)
 {
 brdcastSocket.Close();
 brdcastSocket = null;
 File.AppendAllText("E:\\Exception.txt", ex.ToString() + "\r\nPushMessage\r\n" + DateTime.Now.ToString() + "\r\n");
 continue;
 }

 }
 }
 }
}

网络编程的环境比较复杂,里面很多地方进行了异常处理和判断。这是通过我实际的调试不断完善的,有的bug甚至是放到了真实的服务器上运行了几个小时以后发现的。所以异常处理最好是记录下来,这样能够便于发现问题。

客户端xaml:

<Window x:Class="ChatClient.ChatRoom"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 Title="ChatRoom" Height="550" Width="500"
 Closed="Window_Closed_1" Activated="Window_Activated_1" WindowStartupLocation="CenterScreen">
 <Window.Resources>
 <RoutedUICommand x:Key="SendMessageShortcutKey" Text="Send Message Shortcut Key"></RoutedUICommand>
 </Window.Resources>
 <Window.InputBindings>
 <KeyBinding Gesture="Ctrl+Enter" Command="{StaticResource SendMessageShortcutKey}"></KeyBinding>
 </Window.InputBindings>
 <Window.CommandBindings>
 <CommandBinding Command="{StaticResource SendMessageShortcutKey}" CanExecute="CommandBinding_SendMessage_CanExecute" Executed="CommandBinding_SendMessage_Executed">
 </CommandBinding>
 </Window.CommandBindings>
 <Grid>
 <Grid.RowDefinitions>
 <RowDefinition Height="50"></RowDefinition>
 <RowDefinition Height="250"></RowDefinition>
 <RowDefinition Height="150"></RowDefinition>
 <RowDefinition Height="80"></RowDefinition>
 </Grid.RowDefinitions>
 <StackPanel Grid.Row="0" Orientation="Horizontal">
 <Label Name="lbMsg" Content="设置你的用户名:" Margin="10,10"></Label>
 <TextBox Name="txtName" Width="200" Height="30" FontSize="16"></TextBox>
 <Button Name="btnConnect" Content="连接服务器" Width="100" Height="30" Margin="15,8" Click="btnConnect_Click"></Button>
 </StackPanel>
 <TextBox Grid.Row="1" Name="txtReceiveMsg" Margin="10" BorderBrush="DarkGreen" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Foreground="DarkBlue" IsReadOnly="True"></TextBox>
 <TextBox Grid.Row="2" Name="txtSendMsg" Margin="10,0" AcceptsReturn="True" BorderBrush="DarkBlue" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"></TextBox>
 <Button Grid.Row="3" Name="btnSend" Content="发送(Ctrl+Enter)" Width="100" Height="30" Click="btnSend_Click"></Button>
 </Grid>
</Window>

后台代码:

namespace ChatClient
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class ChatRoom : Window
{
public ChatRoom()
{
InitializeComponent();
}

//窗口闪烁代码
public const UInt32 FLASHW_STOP = 0;
public const UInt32 FLASHW_CAPTION = 1;
public const UInt32 FLASHW_TRAY = 2;
public const UInt32 FLASHW_ALL = 3;
public const UInt32 FLASHW_TIMER = 4;
public const UInt32 FLASHW_TIMERNOFG = 12;
[DllImport(“user32.dll”)]
static extern bool FlashWindowEx(ref FLASHWINFO pwfi);

[DllImport(“user32.dll”)]
static extern bool FlashWindow(IntPtr handle, bool invert);

[DllImport(“user32.dll”)]
public static extern IntPtr GetForegroundWindow();

Socket clientSocket = null;
static Boolean isListen = true;
private void btnSend_Click(object sender, RoutedEventArgs e)
{
SendMessage();
}

private void SendMessage()
{
if (String.IsNullOrWhiteSpace(txtSendMsg.Text.Trim()))
{
MessageBox.Show(“发送内容不能为空哦~”);
return;
}
if (clientSocket != null && clientSocket.Connected)
{
String sendMsg = EncryptionAndDecryption.TripleDESEncrypting(txtSendMsg.Text + “$”);
Byte[] bytesSend = Encoding.UTF8.GetBytes(sendMsg);
clientSocket.Send(bytesSend);
txtSendMsg.Text = “”;
}
else
{
MessageBox.Show(“未连接服务器或者服务器已停止,请联系管理员~”);
return;
}
}
/// <summary>
/// 每一个连接的客户端必须设置一个唯一的用户名,在服务端是把用户名和通信套接字
/// 保存在Dictionary<UserName,ClientSocket>.
/// </summary>
/// <param name=”sender”></param>
/// <param name=”e”></param>
private void btnConnect_Click(object sender, RoutedEventArgs e)
{
if (String.IsNullOrWhiteSpace(txtName.Text.Trim()))
{
MessageBox.Show(“还是设置一个用户名吧,这样别人才能认识你哦~”);
return;
}

if (clientSocket == null || !clientSocket.Connected)
{
try
{
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.BeginConnect(IPAddress.Loopback, 8080, (args) =>
{
if (args.IsCompleted)
{
Byte[] bytesSend = new Byte[4096];
txtName.Dispatcher.BeginInvoke(new Action(() =>
{
String tmp = EncryptionAndDecryption.TripleDESEncrypting(txtName.Text.Trim() + “$”);
bytesSend = Encoding.UTF8.GetBytes(tmp);
if (clientSocket != null && clientSocket.Connected)
{
clientSocket.Send(bytesSend);
txtName.IsEnabled = false;
txtSendMsg.Focus();
Thread th = new Thread(DataFromServer);
th.IsBackground = true;
th.Start();

}
else
{
MessageBox.Show(“服务器已经关闭”);
}
}));
}
}, null);

}
catch (SocketException ex)
{
MessageBox.Show(ex.ToString());
}

}
else
{
MessageBox.Show(“You has already connected with Server”);
}
}

private void ShowMsg(String msg)
{
txtReceiveMsg.Dispatcher.BeginInvoke(new Action(() =>
{
txtReceiveMsg.Text += Environment.NewLine + msg;
txtReceiveMsg.ScrollToEnd();
IntPtr handle = new System.Windows.Interop.WindowInteropHelper(this).Handle;
if (this.WindowState == WindowState.Minimized || handle != GetForegroundWindow())
{
FLASHWINFO fInfo = new FLASHWINFO();

fInfo.cbSize = Convert.ToUInt32(Marshal.SizeOf(fInfo));
fInfo.hwnd = handle;
fInfo.dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG;
fInfo.uCount = UInt32.MaxValue;
fInfo.dwTimeout = 0;

FlashWindowEx(ref fInfo);
}
}));

}

//获取服务端的消息
private void DataFromServer()
{
ShowMsg(“Connected to the Chat Server…”);
isListen = true;
try
{

while (isListen)
{
Byte[] bytesFrom = new Byte[4096];
Int32 len = clientSocket.Receive(bytesFrom);
String dataFromClientTmp = Encoding.UTF8.GetString(bytesFrom, 0, len);
if (!String.IsNullOrWhiteSpace(dataFromClientTmp))
{
String dataFromClient = EncryptionAndDecryption.TripleDESDecrypting(dataFromClientTmp);
if (dataFromClient.StartsWith(“#”) && dataFromClient.EndsWith(“#”))
{
String userName = dataFromClient.Substring(1, dataFromClient.Length – 2);
this.Dispatcher.BeginInvoke(new Action(() =>
{

MessageBox.Show(“用户名:[” + userName + “]已经存在,请尝试其它用户名”);
}));
isListen = false;
txtName.Dispatcher.BeginInvoke(new Action(() =>
{
txtName.IsEnabled = true;
clientSocket = null;
}));

}
else
{
ShowMsg(dataFromClient);
}
}
}
}
catch (SocketException ex)
{
isListen = false;
if (clientSocket != null && clientSocket.Connected)
{
//我没有在客户端关闭连接而是向服务端发送一个消息,在服务器端关闭,这样主要
//为了异常的处理放到服务端。客户端关闭会抛异常,服务端也会抛异常。
clientSocket.Send(Encoding.UTF8.GetBytes(EncryptionAndDecryption.TripleDESEncrypting(“$”)));
MessageBox.Show(ex.ToString());
}

}
}

//这是定义了一个发送的快捷键,WPF的知识
private void CommandBinding_SendMessage_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (String.IsNullOrWhiteSpace(txtSendMsg.Text.Trim()))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}

}

private void CommandBinding_SendMessage_Executed(object sender, ExecutedRoutedEventArgs e)
{
SendMessage();
}

private void Window_Activated_1(object sender, EventArgs e)
{

txtSendMsg.Focus();

}

private void Window_Closed_1(object sender, EventArgs e)
{
if (clientSocket != null && clientSocket.Connected)
{
clientSocket.Send(Encoding.UTF8.GetBytes(EncryptionAndDecryption.TripleDESEncrypting(“$”)));
}
}
}
public struct FLASHWINFO
{

public UInt32 cbSize;
public IntPtr hwnd;
public UInt32 dwFlags;
public UInt32 uCount;
public UInt32 dwTimeout;
}
}

 

好了,到这里告一段落了,一个简单的不能再简单的聊天室就完成了。这里还是有很多细节的处理,我不一定处理的很到位,如果你发现了,请留言告诉我。

截个图,看下运行的效果吧!

注:稍微完善了下,增加了加密和窗口闪烁。大家测试的时候,把加密的去掉可以运行。

作者:张雪飞
出处:https://zhangxuefei.site/p/59
版权说明:欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

10comments

  1. 你好,这个可以处理网络被BT,讯雷等Ping出现的错误吗
    还有在转发时,可以确定数据的完整性吗

  2. EncryptionAndDecryption 你这个是不是自己定义的防止中文乱码的? 还有印象不?漂亮的妹纸?

发表评论

电子邮件地址不会被公开。 必填项已用*标注