用 C# 编写的 TCP/IP 服务器
TCP/IP 服务器,作为 Windows 服务运行,等待客户端请求
本文面向读者
C# 初学者,对网络和协议了解甚少。需要一些线程知识。需要 Microsoft 开发环境(Visual Studio 2003)。
简要描述
通过本文,我想向读者介绍一个由 Windows 服务启动的 TCP/IP 服务器,该服务器可以在 Windows 服务停止时停止。这个 TCP/IP 服务器监听客户端连接请求。当收到客户端的请求时,服务器的任务是将客户端发送的数据读取并存储到 C:\TCP 目录下的一个文件中(文件名也由客户端指定)。
使用的难词
(有经验的读者可以忽略/略读本节)
网络 (Network):一组互联的计算机(“通过电缆物理连接”/“通过卫星通信(即通过互联网)无线连接”)
协议 (Protocol):通信规则集
示例:
假设有两个事物(计算机/人/任何东西)A 和 B。
A first says to B: Hello B.
B then says back to A: Hello A.
A then Says: How are you?
B then says: Good. How about you?
A then says: Good.
A again Says: Bye B.
B says: Bye A
如果上述沟通每次 A 和 B 相遇时都会发生,那么就可以称之为它们之间的问候协议。如果 A 看到 B 时转过脸去(可能和他吵架了)而忽略他;那么上述沟通就不能称为协议,因为上述沟通并非总是发生。
TCP/IP (传输控制协议/互联网协议):它是许多网络用于通信的通信协议。
IP 地址 (I/P Address):互联网/内联网中的每台机器都被赋予了一个唯一的数字,格式为 xxx.xxx.xxx.xxx,其中 xxx 可以是 0 到 127 之间的整数。
Windows 服务 (Windows Service):在 Windows 2000/XP 上,转到“开始”/“控制面板”/“管理工具”/ 我们可以看到“Windows 服务”图标。服务是一种由操作系统自动运行的计算机程序,只要操作系统启动(通常在用户没有任何干预的情况下)。服务通常具有启动、停止、暂停状态。服务启动也可以是自动(操作系统启动计算机时)、手动(必须由用户手动启动)或禁用(不起作用)。
服务器 (Server):一个等待通信对象的程序(既执行侦听也执行通信)。
客户端 (Client):尝试与服务器通信的某人(同样进行侦听和通信)。
(你可能会想,那它们之间有什么区别呢?区别在于等待。我们以餐厅为例。服务器接受顾客(客户端)的点单并送餐,顾客在用餐完毕后离开。但服务器会一直准备好为其他顾客服务,直到他们的工作结束。)
套接字 (Socket):服务器和客户端之间通信的连接点。通常当我们说套接字时,我们指的是服务器上的一个连接点。但客户端也需要一个套接字来与它们的服务器通信。现在要记住的重要一点是,只有一个服务器为多个客户端提供服务。所有客户端都连接到服务器上的同一个点(套接字),但它们自己有不同的独立连接点。让我们以电话为例。当几个人(“客户端”)通过一条电话线给同一个“服务器”打电话时,这个人“服务器”使用他手机上的闪断/切换机制,独立地切换和与不同的“客户端”通话,这样没有人会听到“服务器”与其他人通话的内容。
端口 (Port (Number)):它基本上是一个正整数,用于标识通信点。
套接字与端口的区别:套接字是定义了 IP 地址及其端口号的抽象概念。
程序工作原理:
此程序的工作方式如下。
- 该程序在名为“TCPService”的 Windows 服务启动时启动。
- 当 Windows 服务启动时,TCP/IP 服务器也会启动,并且该服务器将始终处于等待各种客户端连接的状态。
- 每当有客户端连接进来时,服务器就会创建一个“客户端套接字侦听”对象来处理客户端套接字和服务器套接字之间的通信,然后再次进入等待状态以接收另一个连接请求。
- “客户端套接字侦听”对象有一个由服务器启动的线程,该线程负责与客户端的通信。
- 这种通信是按照预定义的协议进行的(请参见下一节)。
- 协议总结是,客户端首先发送一个文件名,然后发送一些数据,服务器必须将这些数据存储在服务器机器的 C:\TCP 目录中,文件名为客户端发送的名称。
- 一旦客户端发送完所有它想发送的内容,它就会与“客户端套接字侦听”对象即服务器断开连接。因此,由服务器创建的这个“客户端套接字侦听”就可以被删除了。
- 服务器还有一个低优先级的线程在运行,负责删除这些“待删除”的“客户端套接字侦听”对象。
- 一旦 Windows 服务停止,服务器也会停止,并且所有客户端连接都会中断。
本程序的协议
协议工作原理如下。
- 客户端首先发送一个以“CRLF”(回车符和换行符="\r\n")结尾的文件名。
- 然后服务器必须将文件名长度返回给客户端进行验证。如果长度与客户端之前发送的长度匹配,则客户端开始发送信息行。否则,客户端将关闭套接字。
- 客户端发送的每一行信息都以“CRLF”结尾。
- 服务器必须将每一行信息存储在之前由客户端发送的文件名所指定的一个文本文件中。
- 作为最后一行信息,客户端发送“[EOF]”行,该行也以“CRLF”("\r\n")结尾。这向服务器发出文件结束的信号,然后服务器将数据的总长度(不包括之前发送的文件名信息行)返回给客户端进行验证。
由于服务器需要为多个客户端执行此操作,因此一旦收到来自客户端的连接请求,它就会为每个客户端请求实例化一个单独的线程来实现上述协议。
程序说明
我将不解释如何使用 C# 在 Microsoft 开发环境中使用 Windows 服务。最好的资源是
服务代码包含以下方法:OnStart
和 OnStop
。
我已将它们编码如下:
/************** TCPService.cs *****************/
protected override void OnStart(string[] args)
{
// Create the Server Object ans Start it.
server = new TCPServer();
server.StartServer();
}
protected override void OnStop()
{
// Stop the Server. Release it.
server.StopServer();
server=null;
}
/**************** TCPServer.cs ******************/
TCPServer is the Server class. Its constructor has following code.
try
{
m_server = new TcpListener(ipNport);
// Create a directory for storing client sent files.
if (!Directory.Exists(TCPSocketListener.DEFAULT_FILE_STORE_LOC))
{
Directory.CreateDirectory(
TCPSocketListener.DEFAULT_FILE_STORE_LOC);
}
}
catch(Exception e)
{
m_server=null;
}
TCPListener
是 .NET 框架用于创建服务器套接字的类。下面的代码用于在不存在时创建“C:\TCP”目录。服务器监听的端口是“ipNport”(30001)。如果我们查看 TCPService
代码,它会调用 TCPServer
的 StartServer
方法。其代码如下:
public void StartServer()
{
if (m_server!=null)
{
// Create a ArrayList for storing SocketListeners before
// starting the server.
m_socketListenersList = new ArrayList();
// Start the Server and start the thread to listen client
// requests.
m_server.Start();
m_serverThread = new Thread(new ThreadStart(ServerThreadStart));
m_serverThread.Start();
// Create a low priority thread that checks and deletes client
// SocktConnection objcts that are marked for deletion.
m_purgingThread = new Thread(new ThreadStart(PurgingThreadStart));
m_purgingThread.Priority=ThreadPriority.Lowest;
m_purgingThread.Start();
}
}
//SocketListener is the class that represents a client communication.
服务器(TCPListener
对象)在调用 StartServer
之前不会启动。请参阅上面的代码中的 m_server.Start()
。我们还可以看到启动了一个线程来侦听客户端请求。它由方法“ServerThreadStart
”表示。我们可以注意到还启动了一个低优先级的线程(清理线程)用于删除“客户端套接字侦听”对象,如果它们变得过时,即服务器和客户端之间的通信已完成。对于上面的清理过程,可能还有其他方法。但此时我选择了这种方法。另一个有趣的事情是,在 C#(在 C/C++ 中也是如此)中,线程与方法相关联。因此,与服务器线程相关联的方法是“ServerThreadStart
”,与清理线程相关联的方法是“PurgingThreadStart
”。
private void ServerThreadStart()
{
// Client Socket variable;
Socket clientSocket = null;
TCPSocketListener socketListener = null;
while(!m_stopServer)
{
try
{
// Wait for any client requests and if there is any
// request from any client accept it (Wait indefinitely).
clientSocket = m_server.AcceptSocket();
// Create a SocketListener object for the client.
socketListener = new TCPSocketListener(clientSocket);
// Add the socket listener to an array list in a thread
// safe fashon.
//Monitor.Enter(m_socketListenersList);
lock(m_socketListenersList)
{
m_socketListenersList.Add(socketListener);
}
//Monitor.Exit(m_socketListenersList);
// Start a communicating with the client in a different
// thread.
socketListener.StartSocketListener();
}
catch (SocketException se)
{
m_stopServer = true;
}
}
}
我将不解释清理线程中会发生什么。如果看上面的代码,第一个有趣的地方是“m_server.AcceptSocket()
”。这是一个阻塞语句。这意味着,除非服务器关闭或收到客户端请求,否则代码执行将在此处被阻塞。如果收到客户端请求,我们可以看到创建了一个“客户端套接字侦听”类的对象(这里是 'TCPSocketListener
' 类)并将其添加到哈希表中。将其添加到哈希表中是因为清理线程应该删除它('TCPSocketListener
' 对象),一旦通信完成。另一行重要的代码是“socketListener.StartSocketListener();
”。这是在“客户端套接字侦听”对象(即 'TCPSocketListener
' 类对象)中启动一个单独的线程。
最后(但同样重要)要提到的是对 'm_socketListenersList
' 哈希表的锁定块。这是必需的,因为可能存在一个时间点,新的连接正在被添加,而一个过期的连接需要被删除。锁定确保了添加和删除都能顺利进行。
public void StopServer()
{
if (m_server!=null)
{
// It is important to Stop the server first before doing
// any cleanup. If not so, clients might being added as
// server is running, but supporting data structures
// (such as m_socketListenersList) are cleared. This might
// cause exceptions.
// Stop the TCP/IP Server.
m_stopServer=true;
m_server.Stop();
// Wait for one second for the the thread to stop.
m_serverThread.Join(1000);
// If still alive; Get rid of the thread.
if (m_serverThread.IsAlive)
{
m_serverThread.Abort();
}
m_serverThread=null;
m_stopPurging=true;
m_purgingThread.Join(1000);
if (m_purgingThread.IsAlive)
{
m_purgingThread.Abort();
}
m_purgingThread=null;
// Free Server Object.
m_server = null;
// Stop All clients.
StopAllSocketListers();
}
}
//Above method is self explanatory.
//'StopAllSocketListers();' is method to delete all existing
//client socket connections.
/**************** TCPSocketListener.cs ******************/
//Let us see the constructor of this class which is
//very simple and doesn't need any explanation.
public TCPSocketListener(Socket clientSocket)
{
m_clientSocket = clientSocket;
}
//Let us see the 'StartSocketListener' method.
public void StartSocketListener()
{
if (m_clientSocket!= null)
{
m_clientListenerThread =
new Thread(new ThreadStart(SocketListenerThreadStart));
m_clientListenerThread.Start();
}
}
//What all this method does is to start a thread by
//associating it with the method 'SocketListenerThreadStart'
private void SocketListenerThreadStart()
{
int size=0;
Byte [] byteBuffer = new Byte[1024];
m_lastReceiveDateTime = DateTime.Now;
m_currentReceiveDateTime = DateTime.Now;
Timer t= new Timer(new TimerCallback(CheckClientCommInterval),
null,15000,15000);
while (!m_stopClient)
{
try
{
size = m_clientSocket.Receive(byteBuffer);
m_currentReceiveDateTime=DateTime.Now;
ParseReceiveBuffer(byteBuffer, size);
}
catch (SocketException se)
{
m_stopClient=true;
m_markedForDeletion=true;
}
}
t.Change(Timeout.Infinite, Timeout.Infinite);
t=null;
}
有几个重要的点值得一提。'CheckClientCommInterval
' 是另一个线程方法,它负责在客户端超过 15 秒没有响应时终止 'SocketListenerThreadStart()
' 线程方法。- '
m_clientSocket.Receive(byteBuffer)
' 这是从客户端接收数据的阻塞接收。 'ParseReceiveBuffer
' 方法负责解析客户端发送的数据,其细节我在这里不描述,因为它实现了上面描述的协议。
//********************* Client Code ***********************
//Here I am giving the Sample client code.
//Which I feel doesn't need any explanation.
class TCPClient
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
TCPClient client = null;
client = new TCPClient("SatyaTest.txt\r\n");
client = new TCPClient("SatyaTest1.txt\r\n");
client = new TCPClient("SatyaTest2.txt\r\n");
client = new TCPClient("SatyaTest3.txt\r\n");
client = new TCPClient("SatyaTest4.txt\r\n");
client = new TCPClient("SatyaTest5.txt\r\n");
}
private String m_fileName=null;
public TCPClient(String fileName)
{
m_fileName=fileName;
Thread t = new Thread(new ThreadStart(ClientThreadStart));
t.Start();
}
private void ClientThreadStart()
{
Socket clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp );
clientSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"),31001));
// Send the file name.
clientSocket.Send(Encoding.ASCII.GetBytes(m_fileName));
// Receive the length of the filename.
byte [] data = new byte[128];
clientSocket.Receive(data);
int length=BitConverter.ToInt32(data,0);
clientSocket.Send(Encoding.ASCII.GetBytes(m_fileName+":"+
"this is a test\r\n"));
clientSocket.Send(Encoding.ASCII.GetBytes(m_fileName+":"+
"THIS IS "));
clientSocket.Send(Encoding.ASCII.GetBytes("ANOTHRER "));
clientSocket.Send(Encoding.ASCII.GetBytes("TEST."));
clientSocket.Send(Encoding.ASCII.GetBytes("\r\n"));
clientSocket.Send(Encoding.ASCII.GetBytes(m_fileName+":"+
"TEST.\r\n"+m_fileName+":"+"TEST AGAIN.\r\n"));
clientSocket.Send(Encoding.ASCII.GetBytes("[EOF]\r\n"));
// Get the total length
clientSocket.Receive(data);
length=BitConverter.ToInt32(data,0);
clientSocket.Close();
}
}
我希望我已经解释了程序的所有重要部分。如果你们中有人觉得我做得有严重错误或不清楚的地方,请随时指正。我会尽力回应。