首先让我们写一个C/S多线程网络程序,通过这个程序了解初学网络编程是应该注意的几个问题。
下面开始设计服务器端的业务逻辑代码:
首先,值得强调的是,要在该文件的顶端添加多线程和网络的命名空间语句:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
//可以套接字
using System.Net.Sockets;
//可以使用多线程
using System.Threading;
接写来,设计类Threadtcpserver。代码如下:
class Threadtcpserver
{
private Socket server;
public Threadtcpserver()
{
//初始化IP地址
IPAddress local = IPAddress.Parse("127.0.0.1");
IPEndPoint iep = new IPEndPoint(local, 13000);
server = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//将套接字与本地终结点绑定
server.Bind(iep);
//在本地13000端口号上进行监听
server.Listen(20);
Console.WriteLine("等待客户机进行连接...");
while (true)
{
//得到包含客户端信息的套接字
Socket client = server.Accept();
//创建消息线程服务对象
ClientThread newclient = new ClientThread(client);
//把ClientThread类的ClientService方法委托给线程
Thread newthread = new Thread(new ThreadStart(newclient.ClientService));
//启动消息服务进程
newthread.Start();
}
}
}
上面这段代码的业务逻辑是:
1.创建套接字Server,并将其与本地这结点iep进行绑定。然后,在13000端口上监听是否有新的客户端进行连接。
2.在无限循环中有一个阻塞的方法Accept(),该方法直到有新客户端连接到服务器上时,把客户端的套接字信息传递给client对象。否则,将阻塞直到有客户机进行连接。
3.ClientThread类负责处理客户端与服务器之间的通信。先把客户端的套接字句柄传递给负责消息服务的ClientThread类。然后,把ClientThread类的ClientService方法委托给线程,并启动线程。
然后开始编写ClientThread类的代码:
class ClientThread
{
//connection变量表示连接数
public static int connection = 0;
public Socket service;
int i;
//构造函数
public ClientThread(Socket clientsocket)
{
//service对象接管对消息的控制
this.service = clientsocket;
}
public void ClientService()
{
String data = null;
byte[] bytes = new byte[1024];
//如果Socket不是空,则连接数加1
if (service != null)
{
connection++;
}
Console.WriteLine("新客户建立连接:{0}个连接数", connection);
while ((i=service.Receive(bytes)) != 0)
{
data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
Console.WriteLine("收到的数据:{0}", data);
//处理客户端发来的消息,这里转化为大写字母
data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
//发送一条答应消息
service.Send(msg);
Console.WriteLine("发送的数据:{0}", data);
}
//关闭套接字
service.Close();
connection--;
Console.WriteLine("客户关闭连接:{0}个连接数", connection);
}
这段代码的业务逻辑:
1.在构造函数中得到接收到的客户端套接字client,以后就通过service句柄处理消息的接收和发送。
2.ClientService方法是委托给线程的,此方法进行消息的处理工作。在这里实现的功能是,先从客户端接收一条消息,然后把这条消息转换为大写字母,并立即发送一条应答的消息,也就是所谓的echo技术,通常用来进行消息之间的测试。
3.通过connections变量来记录活动连接数。当有新增连接或有断开连接的情况发生时,都会体现connection是的变化。
主函数代码如下:
static void Main(string[] args)
{
Threadtcpserver instance = new Threadtcpserver();
}
Main函数十分简单,生成一个Threadtcpsever实例,然后构造函数就会一步一步地展开,开始执行具体的业务逻辑。
编写完服务器端的程序后,只要再配置一个客户端程序即可。下面编写客户端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
Socket client;
byte[] buf = new byte[1024];
string input;
IPAddress local = IPAddress.Parse("127.0.0.1");
IPEndPoint iep = new IPEndPoint(local, 13000);
try
{
client = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
client.Connect(iep);
}
catch (SocketException)
{
Console.WriteLine("无法连接到服务器!");
return;
}
finally
{
}
while (true)
{
//在控制台上输入一条消息
input = Console.ReadLine();
//如果输入exit,可以断开服务器的连接
if (input == "exit")
{
break;
}
client.Send(Encoding.ASCII.GetBytes(input));
//得到实际收到的字节总数
int rec = client.Receive(buf);
Console.WriteLine(Encoding.ASCII.GetString(buf, 0, rec));
}
Console.WriteLine("断开与服务器的链接");
client.Close();
}
}
}
客户端是比较简单的,只有一个Main()函数。下面来分析一下这段代码的功能:
1.创建套接字,并通过connect方法连接到本地终结点。当连接建立以后,便可以与服务器进行通信了。
2.在客户端上等待用户输入一条信息,该消息会发送到服务器创建的消息服务线程上的ClientService方法进行处理。
运行界面:
让网络通信代码更强壮
通常,程序都需要进行后期维护和扩展。下面将通过在程序中加入异常处理的机制来加强该程序的健壮性。可以通过使用try和catch结构来包含可能会引发错误的代码。
try
{
client = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
client.Connect(iep);
}
catch (SocketException)
{
Console.WriteLine("无法连接到服务器!");
return;
}
finally
{
Socket.close();
}
数据缓冲区处理方法
对于网络编程来说,如何处理好缓冲区这个问题一直是颇具有挑战性的。
1.合理的缓冲区处理
在多线程那段代码里,我们并没有对消息处理的细节做深入的分析,现在来看一下到底是怎么做的。
while ((i=service.Receive(bytes)) != 0)
{
data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
Console.WriteLine("收到的数据:{0}", data);
//处理客户端发来的消息,这里转化为大写字母
data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
//发送一条答应消息
service.Send(msg);
Console.WriteLine("发送的数据:{0}", data);
}
这里的一个细节是,通过使用变量i得到实际从缓冲区取到的数据总数。在吧字节数组bytes转换成字符串data的过程中,可以清晰的看到,使用了GetString函数的带三个参数版本的重载方法。这样做有什么道理吗?如果把GetString方法的后两个参数去掉,会有什么结果?
不合理的缓冲区处理
在上文的代码中,去掉后两个参数并测试。
我们看到服务器端在接收到数据后多出了很多空行。
真正的原因是Recive方法会一次性把系统缓冲区内的内容取到数据缓冲区,得到数据量取决于缓冲区的大小,即bytes的大小。
如果把缓冲区大小设置的过小会出现什么状况呢?
我们把bytes重新设置:byte[] bytes = new byte[2];
当输入TEST时,系统会分两次进行数据传送。。即调用了两次Recive方法。
所以在处理缓冲区问题是,必须将问题考虑周全。
下面开始设计服务器端的业务逻辑代码:
首先,值得强调的是,要在该文件的顶端添加多线程和网络的命名空间语句:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
//可以套接字
using System.Net.Sockets;
//可以使用多线程
using System.Threading;
接写来,设计类Threadtcpserver。代码如下:
class Threadtcpserver
{
private Socket server;
public Threadtcpserver()
{
//初始化IP地址
IPAddress local = IPAddress.Parse("127.0.0.1");
IPEndPoint iep = new IPEndPoint(local, 13000);
server = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//将套接字与本地终结点绑定
server.Bind(iep);
//在本地13000端口号上进行监听
server.Listen(20);
Console.WriteLine("等待客户机进行连接...");
while (true)
{
//得到包含客户端信息的套接字
Socket client = server.Accept();
//创建消息线程服务对象
ClientThread newclient = new ClientThread(client);
//把ClientThread类的ClientService方法委托给线程
Thread newthread = new Thread(new ThreadStart(newclient.ClientService));
//启动消息服务进程
newthread.Start();
}
}
}
上面这段代码的业务逻辑是:
1.创建套接字Server,并将其与本地这结点iep进行绑定。然后,在13000端口上监听是否有新的客户端进行连接。
2.在无限循环中有一个阻塞的方法Accept(),该方法直到有新客户端连接到服务器上时,把客户端的套接字信息传递给client对象。否则,将阻塞直到有客户机进行连接。
3.ClientThread类负责处理客户端与服务器之间的通信。先把客户端的套接字句柄传递给负责消息服务的ClientThread类。然后,把ClientThread类的ClientService方法委托给线程,并启动线程。
然后开始编写ClientThread类的代码:
class ClientThread
{
//connection变量表示连接数
public static int connection = 0;
public Socket service;
int i;
//构造函数
public ClientThread(Socket clientsocket)
{
//service对象接管对消息的控制
this.service = clientsocket;
}
public void ClientService()
{
String data = null;
byte[] bytes = new byte[1024];
//如果Socket不是空,则连接数加1
if (service != null)
{
connection++;
}
Console.WriteLine("新客户建立连接:{0}个连接数", connection);
while ((i=service.Receive(bytes)) != 0)
{
data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
Console.WriteLine("收到的数据:{0}", data);
//处理客户端发来的消息,这里转化为大写字母
data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
//发送一条答应消息
service.Send(msg);
Console.WriteLine("发送的数据:{0}", data);
}
//关闭套接字
service.Close();
connection--;
Console.WriteLine("客户关闭连接:{0}个连接数", connection);
}
这段代码的业务逻辑:
1.在构造函数中得到接收到的客户端套接字client,以后就通过service句柄处理消息的接收和发送。
2.ClientService方法是委托给线程的,此方法进行消息的处理工作。在这里实现的功能是,先从客户端接收一条消息,然后把这条消息转换为大写字母,并立即发送一条应答的消息,也就是所谓的echo技术,通常用来进行消息之间的测试。
3.通过connections变量来记录活动连接数。当有新增连接或有断开连接的情况发生时,都会体现connection是的变化。
主函数代码如下:
static void Main(string[] args)
{
Threadtcpserver instance = new Threadtcpserver();
}
Main函数十分简单,生成一个Threadtcpsever实例,然后构造函数就会一步一步地展开,开始执行具体的业务逻辑。
编写完服务器端的程序后,只要再配置一个客户端程序即可。下面编写客户端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
Socket client;
byte[] buf = new byte[1024];
string input;
IPAddress local = IPAddress.Parse("127.0.0.1");
IPEndPoint iep = new IPEndPoint(local, 13000);
try
{
client = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
client.Connect(iep);
}
catch (SocketException)
{
Console.WriteLine("无法连接到服务器!");
return;
}
finally
{
}
while (true)
{
//在控制台上输入一条消息
input = Console.ReadLine();
//如果输入exit,可以断开服务器的连接
if (input == "exit")
{
break;
}
client.Send(Encoding.ASCII.GetBytes(input));
//得到实际收到的字节总数
int rec = client.Receive(buf);
Console.WriteLine(Encoding.ASCII.GetString(buf, 0, rec));
}
Console.WriteLine("断开与服务器的链接");
client.Close();
}
}
}
客户端是比较简单的,只有一个Main()函数。下面来分析一下这段代码的功能:
1.创建套接字,并通过connect方法连接到本地终结点。当连接建立以后,便可以与服务器进行通信了。
2.在客户端上等待用户输入一条信息,该消息会发送到服务器创建的消息服务线程上的ClientService方法进行处理。
运行界面:
让网络通信代码更强壮
通常,程序都需要进行后期维护和扩展。下面将通过在程序中加入异常处理的机制来加强该程序的健壮性。可以通过使用try和catch结构来包含可能会引发错误的代码。
try
{
client = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
client.Connect(iep);
}
catch (SocketException)
{
Console.WriteLine("无法连接到服务器!");
return;
}
finally
{
Socket.close();
}
数据缓冲区处理方法
对于网络编程来说,如何处理好缓冲区这个问题一直是颇具有挑战性的。
1.合理的缓冲区处理
在多线程那段代码里,我们并没有对消息处理的细节做深入的分析,现在来看一下到底是怎么做的。
while ((i=service.Receive(bytes)) != 0)
{
data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
Console.WriteLine("收到的数据:{0}", data);
//处理客户端发来的消息,这里转化为大写字母
data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
//发送一条答应消息
service.Send(msg);
Console.WriteLine("发送的数据:{0}", data);
}
这里的一个细节是,通过使用变量i得到实际从缓冲区取到的数据总数。在吧字节数组bytes转换成字符串data的过程中,可以清晰的看到,使用了GetString函数的带三个参数版本的重载方法。这样做有什么道理吗?如果把GetString方法的后两个参数去掉,会有什么结果?
不合理的缓冲区处理
在上文的代码中,去掉后两个参数并测试。
我们看到服务器端在接收到数据后多出了很多空行。
真正的原因是Recive方法会一次性把系统缓冲区内的内容取到数据缓冲区,得到数据量取决于缓冲区的大小,即bytes的大小。
如果把缓冲区大小设置的过小会出现什么状况呢?
我们把bytes重新设置:byte[] bytes = new byte[2];
当输入TEST时,系统会分两次进行数据传送。。即调用了两次Recive方法。
所以在处理缓冲区问题是,必须将问题考虑周全。