using System;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
namespace EasyChat_Server
{
class Listener
{
#region 字段定义
/// <summary>
/// 服务器程序使用的端口,默认为8888
/// </summary>
private int _port = 8888;
/// <summary>
/// 接收数据缓冲区大小64K
/// </summary>
private const int _maxPacket = 64 * 1024;
/// <summary>
/// 服务器端的监听器
/// </summary>
private TcpListener _tcpl = null;
/// <summary>
/// 保存所有客户端会话的哈希表
/// </summary>
private Hashtable _transmit_tb = new Hashtable();
#endregion
#region 服务器方法
/// <summary>
/// 关闭监听器并释放资源
/// </summary>
public void Close()
{
if (_tcpl != null)
{
_tcpl.Stop();//如果不为空 就停止 tcplistener
}
//关闭客户端连接并清理资源
if (_transmit_tb.Count != 0)//客户端会话的哈希表
{
foreach (Socket session in _transmit_tb.Values)
{
session.Shutdown(SocketShutdown.Both);
}
_transmit_tb.Clear();
_transmit_tb = null;
}
}
/// <summary>
/// 配置监听端口号
/// </summary>
public void GetConfig()
{
string portParam;
Console.Write("请输入监听端口,直接回车则接受默认端口8888: ");
//portParam = Console.ReadLine();
portParam = Console.ReadLine();
if (portParam != string.Empty)
{
if (!int.TryParse(portParam, out _port) || _port < 1023 || _port > 65535)//很是精炼的语句
{
_port = 8888;
Console.WriteLine("端口号不合法,默认端口号被接受!");
}
}
}
/// <summary>
/// 序列化在线列表,向客户端返回序列化后的字节数组
/// </summary>
/// <returns>序列化后的字节数组</returns>
private byte[] SerializeOnlineList()
{
StringCollection onlineList = new StringCollection();
foreach (object o in _transmit_tb.Keys)
{
onlineList.Add(o as string);//转换语句
}
IFormatter format = new BinaryFormatter();//以二进制格式将对象或整个连接对
//象图形序列化和反序列化。
MemoryStream stream = new MemoryStream();
format.Serialize(stream, onlineList);//保持到内存流
byte[] ret = stream.ToArray();
stream.Close();
return ret;
}
/// <summary>
/// 提取命令
/// 格式为两个一位整数拼接成的字符串。
/// 第一位为0表示客户机向服务器发送的命令,为1表示服务器向客户机发送的命令。
/// 第二位表示命令的含义,具体如下:
/// "01"-离线
/// "02"-请求在线列表
/// "03"-请求对所有人闪屏振动
/// "04"-请求对指定用户闪屏振动
/// "05"-请求广播消息
/// default-转发给指定用户
/// </summary>
/// <param name="s">要解析的包含命令的byte数组,只提取前两个字节</param>
/// <returns>拼接成的命令</returns>
private string DecodingBytes(byte[] s)
{
return string.Concat(s[0].ToString(), s[1].ToString());
}
/// <summary>
/// 线程执行体,转发消息-------------------
/// </summary>
/// <param name="obj">传递给线程执行体的用户名,用以与用户通信</param>
private void ThreadFunc(object obj)
{
//通过转发表得到当前用户套接字
Socket clientSkt = _transmit_tb[obj] as Socket;
while (true)
{
try
{
//接受第一个数据包。
//仅有两种情况,一为可以识别的命令格式,否则为要求转发给指定用户的用户名。
//这里的实现不够优雅,但不失为此简单模型的一个解决之道。
byte[] _cmdBuff = new byte[128];
clientSkt.Receive(_cmdBuff);
string _cmd = DecodingBytes(_cmdBuff);
/// "01"-离线
/// "02"-请求在线列表
/// "03"-请求对所有人闪屏振动
/// "04"-请求对指定用户闪屏振动
/// "05"-请求广播消息
/// default-转发给指定用户
switch (_cmd)
{
case "01":
{
_transmit_tb.Remove(obj);
string svrlog = string.Format("[系统消息]用户 {0} 在 {1} 已断开... 当前在线人数: {2}/r/n/r/n", obj, DateTime.Now, _transmit_tb.Count);
Console.WriteLine(svrlog);
//向所有客户机发送系统消息
foreach (DictionaryEntry de in _transmit_tb)
{
string _clientName = de.Key as string;
Socket _clientSkt = de.Value as Socket;
_clientSkt.Send(Encoding.Unicode.GetBytes(svrlog));//给其他用户通知现在上线的用户
}
Thread.CurrentThread.Abort();
break;
}
case "02":
{
byte[] onlineBuff = SerializeOnlineList();
//先发送响应信号,用于客户机的判断,"11"表示服务发给客户机的更新在线列表的命令
clientSkt.Send(new byte[] { 1, 1 });
clientSkt.Send(onlineBuff);
break;
}
case "03":
{
string displayTxt = string.Format("[系统提示]用户 {0} 向您发送了一个闪屏振动。/r/n/r/n", obj);
foreach (DictionaryEntry de in _transmit_tb)
{
string _clientName = de.Key as string;
Socket _clientSkt = de.Value as Socket;
if (!_clientName.Equals(obj))
{
//先发送响应信号,用于客户机的判断,"12"表示服务发给客户机的闪屏振动的命令
_clientSkt.Send(new byte[] { 1, 2 });
_clientSkt.Send(Encoding.Unicode.GetBytes(displayTxt));
}
}
break;
}
case "04":
{
string _receiver = null;
byte[] _receiverBuff = new byte[128];
clientSkt.Receive(_receiverBuff);
_receiver = Encoding.Unicode.GetString(_receiverBuff).TrimEnd('/0');
string displayTxt = string.Format("[系统提示]用户 {0} 向您发送了一个闪屏振动。/r/n/r/n", obj);
//通过转发表查找接收方的套接字
if (_transmit_tb.ContainsKey(_receiver))
{
Socket receiverSkt = _transmit_tb[_receiver] as Socket;
receiverSkt.Send(new byte[] { 1, 2 });
receiverSkt.Send(Encoding.Unicode.GetBytes(displayTxt));
}
else
{
string sysMessage = string.Format("[系统消息]您刚才的闪屏振动没有发送成功。/r/n可能原因:用户 {0} 已离线或者网络阻塞。/r/n/r/n", _receiver);
clientSkt.Send(Encoding.Unicode.GetBytes(sysMessage));
}
break;
}
case "05":
{
byte[] _msgBuff = new byte[_maxPacket];
clientSkt.Receive(_msgBuff);
foreach (DictionaryEntry de in _transmit_tb)
{
string _clientName = de.Key as string;
Socket _clientSkt = de.Value as Socket;
if (!_clientName.Equals(obj))
{
_clientSkt.Send(_msgBuff);
}
}
break;
}
default:
{
//以上都不是则表明第一个包是接收方用户名,继续接受第二个消息正文数据包
string _receiver = Encoding.Unicode.GetString(_cmdBuff).TrimEnd('/0');
byte[] _packetBuff = new byte[_maxPacket];
clientSkt.Receive(_packetBuff);
if (_transmit_tb.ContainsKey(_receiver))
{
//通过转发表查找接收方的套接字
Socket receiverSkt = _transmit_tb[_receiver] as Socket;
receiverSkt.Send(_packetBuff);
}
else
{
string sysMessage = string.Format("[系统消息]您刚才的内容没有发送成功。/r/n可能原因:用户 {0} 已离线或者网络阻塞。/r/n/r/n", _receiver);
clientSkt.Send(Encoding.Unicode.GetBytes(sysMessage));
}
break;
}
}
}
catch (SocketException)
{
_transmit_tb.Remove(obj);
string svrlog = string.Format("[系统消息]用户 {0} 的客户端在 {1} 意外终止!当前在线人数:{2}/r/n/r/n", obj, DateTime.Now, _transmit_tb.Count);
Console.WriteLine(svrlog);
//向所有客户机发送系统消息
foreach (DictionaryEntry de in _transmit_tb)
{
string _clientName = de.Key as string;
Socket _clientSkt = de.Value as Socket;
_clientSkt.Send(Encoding.Unicode.GetBytes(svrlog));
}
Console.WriteLine();
Thread.CurrentThread.Abort();
}
}
}
/// <summary>
/// 启动监听,轮询监听客户机请求并将客户端套接字存入转发表
/// </summary>
public void StartUp()
{
IPAddress _ip = Dns.GetHostAddresses(Dns.GetHostName())[0];//可能有多个,此时的IP是本地IP
_tcpl = new TcpListener(_ip, _port);
_tcpl.Start();//开始侦听传入的连接请求。
Console.WriteLine("服务器已启动,正在监听.../n");
Console.WriteLine(string.Format("服务器IP:{0}/t端口号:{1}/n", _ip, _port));
while (true)
{
byte[] packetBuff = new byte[_maxPacket];// 接收数据缓冲区大小64K
//AcceptSocket 是一个阻止方法,该方法返回可用于发送和接收数据的 Socket。
//如果希望避免阻止,请使用 Pending 方法来确定传入连接队列中的连接请求是否可用。
//返回的 Socket 是使用远程主机的 IP 地址和端口号初始化的。
//您可以使用 Socket 类中任何可用的 Send 和 Receive 方法与远程主机进行通信。
//------------使用 Socket 后,请确保调用其 Close 方法。如果应用程序相对简单,-----------
//请考虑使用 AcceptTcpClient 方法代替 AcceptSocket 方法。
//TcpClient 提供了一些通过网络在阻止同步模式下发送和接收数据的简单方法。
Socket newClient = _tcpl.AcceptSocket();//本地TcpListener 接受新的请求
//返回的 Socket 是使用远程主机的 IP 地址和端口号初始化的
newClient.Receive(packetBuff);//将接受的远程SOCKET,将数据存入接受缓冲区
string userName = Encoding.Unicode.GetString(packetBuff).TrimEnd('/0');
//验证是否为唯一用户,有了就发送失败命令给请求客户
if (_transmit_tb.Count != 0 && _transmit_tb.ContainsKey(userName))
{
newClient.Send(Encoding.Unicode.GetBytes("cmd::Failed"));
continue;
}
else
{
newClient.Send(Encoding.Unicode.GetBytes("cmd::Successful"));
}
//将新连接加入转发表并创建线程为其服务
_transmit_tb.Add(userName, newClient);
//-----------------------
string svrlog = string.Format("[系统消息]新用户 {0} 在 {1} 已连接... 当前在线人数: {2}/r/n/r/n", userName, DateTime.Now, _transmit_tb.Count);
Console.WriteLine(svrlog);
//------------------------
Thread clientThread = new Thread(new ParameterizedThreadStart(ThreadFunc));
clientThread.Start(userName);
//向所有客户机发送系统消息,说有人上线了
foreach (DictionaryEntry de in _transmit_tb)
{
string _clientName = de.Key as string;
Socket _clientSkt = de.Value as Socket;
if (!_clientName.Equals(userName))
{
_clientSkt.Send(Encoding.Unicode.GetBytes(svrlog));
}
}
}
}
#endregion
}
}