序:实现一个基于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()来向彼此发送消息。
后台代码:
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; } } } } }
后台代码:
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; } }