以前没写完不敢拿出来示人,怕丢人!放在硬盘里快发霉了。节选完整的一节!希望对初学者能举一反四、五.....! Download!!!
Socket通信基础
既然是网络游戏那自然网络的通信是基本的需要了。关于Socket的编程还是一片空白,以前没接触过呀。还是从网上下载几本了解一下。
《TCPIP Sockets in C#》 《C# Network Programming》《Network Programing In .NET with C# and Visual Basic.NET》MSDN .....
英文的,头痛中……
看看里面例子.....
还是不太明白.....
自己动手写代码玩玩.....
看来dotNET对于Socket编程都有封装好的类,直接使用就好。
大概明白了服务器负责通过指定的端口监听客户端发来的数据,客户端通过域名或IP与指定的端口连接到服务器,向服务器发送数据或接收服务器发回的数据。
先跟着书做几个简单的练习可以加深对Socket编程的印象。
练习1:客户端发送数据,服务器端接收数据并显示。
先写客户端吧比较简单
- static void Main(string[] args)
- {
- Console.WriteLine("输入发送的文字:");
- byte[] sendBuffer = Encoding.ASCII.GetBytes(Console.ReadLine());
- TcpClient client = new TcpClient("127.0.0.1", 9008);
- NetworkStream netStream = client.GetStream();
- netStream.Write(sendBuffer, 0, sendBuffer.Length);
- netStream.Close();
- client.Close();
- Console.ReadKey();
- }
第4行 接收控制台的输入使用Encoding.ASCII.GetBytes将输入的字符串转换成byte类型的数组。
第5行 建立TcpClient客户端连接对象,指定ip和端口就行。
第6行 通过客户端获取网络数据流NetworkStream对象。
第7行 使用NetworkStream的Write方法发送数据。
TcpClient 类为TCP 网络服务提供客户端连接。
参考http://msdn.microsoft.com/zh-cn/library/system.net.sockets.tcpclient(VS.80).aspx
NetworkStream 类提供用于网络访问的基础数据流。
参考http://msdn.microsoft.com/zh-cn/library/system.net.sockets.networkstream(VS.80).aspx
服务器端代码
- static void Main(string[] args)
- {
- byte[] rcvBuffer = new byte[1024];
- TcpListener listener = new TcpListener(IPAddress.Any, 9008);
- listener.Start();
- TcpClient client = listener.AcceptTcpClient();
- NetworkStream netStream = client.GetStream();
- string revStr;
- int revBytes = 0;
- //从 NetworkStream 读取数据,如果没有数据返回0
- revBytes = netStream.Read(rcvBuffer, 0, rcvBuffer.Length);
- revStr = Encoding.ASCII.GetString(rcvBuffer, 0, revBytes);
- Console.WriteLine("接收数据:" + revStr);
- netStream.Close();
- client.Close(); //关闭客户端
- listener.Stop(); //停止侦听
- }
Line 3 创建TcpListener类对象来侦听和接收客户端的连接请求。
Line 5 Start 方法用来开始侦听传入的连接请求。Start 将对传入连接进行排队,直至您调用 Stop 方法或它已经完成 MaxConnections(最大连接数) 排队为止。可使用 AcceptSocket 或 AcceptTcpClient 从传入连接请求队列提取连接。这两种方法将阻止。如果要避免阻止,可首先使用 Pending 方法来确定队列中是否有可用的连接请求。
上面这段话摘自MSDN原文。可以这样理解这段话,好比是去银行(服务器端)取钱,假如银行就一个服务窗口,那么好多个储户(客户端)去取钱就要排队喽。窗口服务人员(AcceptTcpClient或AcceptSocket)每次只能服务一个储户,服务完这个才能到下一个。直到所有的储户都取完钱。从窗口的监控(Pending)里就可以了解到是不是有人来取钱哟。
那为了提高工作效率银行又砸了墙加开一个服务窗口,这才能同时服务于两个储户。现实中不可能100个用户取钱,银行就开100个服务窗口,但是程序里就很容易做到了,当然用线程来解决喽为每个客户端开一个线程来处理与服务器的连接问题。这个问题稍后在讨论吧。
Line 6 AcceptTcpClient 方法接受挂起的连接请求,AcceptTcpClient 是一个阻止方法,该方法返回可用于发送和接收数据的 TcpClient。
Line 7 使用 TcpClient.GetStream 方法来获取已返回的 TcpClient 的 NetworkStream。
Line 11 从数据流中读取数据。
TcpListener 类提供一些简单方法,用于在阻止同步模式下侦听和接受传入连接请求。
参考http://msdn.microsoft.com/zh-cn/library/system.net.sockets.tcplistener(VS.80).aspx
注意
如果要编写相对简单的应用程序,而且不需要最高的性能,则可以考虑使用 TcpClient、 TcpListener 和 UdpClient。这些类为 Socket 通信提供了更简单、对用户更友好的接口。
阻止同步模式的概念(摘在网络)
阻止是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。同步与阻止的区别就是同步很多时候线程还是激活的。
运行上面的例子我们可以看到,Server端在运行以后就被挂起了,直到接收到客户端的数据程序才会继续运行,然后结束。
第一个通信程序运行成功,通过网络来传送数据就是这么简单啦!
继续往下进行,让服务器和客户端一直保持运行,客户端不停的输出数据,服务器端持续的接收发送过来的数据并显示。
客户端代码的改进:
string input;
while (true)
{
Console.WriteLine("输入发送的文字:");
input = Console.ReadLine();
if (input == "quit")
break;
byte[] sendBuffer = Encoding.ASCII.GetBytes(input);
netStream.Write(sendBuffer, 0, sendBuffer.Length);
}
加上while循环,客户端持续输入要发送的数据直到输入quit退出。
服务器端代码的改进:
while (true)
{
//从 NetworkStream 读取数据,如果没有数据返回0(tcp closed)
revBytes = netStream.Read(rcvBuffer, 0, rcvBuffer.Length);
if (revBytes == 0)
break;
revStr = Encoding.ASCII.GetString(rcvBuffer, 0, revBytes);
Console.WriteLine("接收数据:" + revStr);
}
也是一样加入while循环,不停的接收客户端发来的数据,直到没有数据发送。
游戏里的通讯一般都是双向的,例如游戏的登录过程,客户端将用户的登录名和密码发送至服务器,服务器验证登录名和密码是否正确。如果正确告诉客户端可以登录,否则拒绝客户端登录,并告知原因。当然,以现在学会的知识还写不出来这个过程。那继续我们的探索之路吧。
服务器端向客户端发送数据与客户端向服务器端发送数据的方法是一样的,都可以使用NetworkSterm类的Write方法。
我们来实现这样的功能,当客户端连接到服务器后发送欢迎信息。
客户端:
NetworkStream netStream = client.GetStream();
//接受服务器欢迎信息
byte []welcome = new byte[30];
netStream.Read(welcome, 0, welcome.Length);
Console.WriteLine(Encoding.ASCII.GetString(welcome));
服务器端:
NetworkStream netStream = client.GetStream();
//向客户端发送欢迎信息
byte[]welcome = Encoding.ASCII.GetBytes("欢迎光临战国游戏!");
netStream.Write(welcome,0,welcome.Length);
编译....
运行....
轻松搞定!怎么接收到的全是"?"号呢!仔细观察一下不难发现是编码的问题。把ASCII换成Unicode就行啦。
测试 #1:
服务器启动,客户端启动。
客户端收到欢迎信息。
发送数据hello,服务器收到。
发送how you doing!
服务器收到。
....
quit按任意键退出。
服务器端退出。
测试#1结束。
测试 #2:
服务器启动,客户端启动。
客户端收到欢迎信息。
发送数据hello,服务器收到。
发送how are you doing!
服务器收到。
开启第二个客户端。
没有欢迎信息,不能输入数据。
(不知连上否)
quit按任意键退出。
服务器端退出。
2号客户端发出异常。
测试#1结束。
问题描述:
- 服务器端没有友好的信息提示客户端已经连接。
- 到目前为止我们设计的服务端与客户端是1对1的,也就是说服务器端当前只能处理一个客户端的请求。而实际网络游戏是多用户的,服务器要几乎同步处理成千上万个客户端的请求。
解决问题1:
查看TcpClient的Connected属性可以表示当前的连接特性。
TcpClient. Client.RemoteEndPoint.ToString()可以显示客户端IP和端口号
服务器代码调整:
- TcpClient client = listener.AcceptTcpClient(); //从Start中提取排队的客户端
- if (client.Connected)
- {
- Console.WriteLine("客户端已连接,IP:{0}",
- client.Client.RemoteEndPoint.ToString());
- NetworkStream netStream = client.GetStream();
- .....
- ....
- Console.WriteLine("接收数据:" + revStr);
- }
- netStream.Close();
- client.Close(); //关闭客户端
-
}
解决问题2:
如何解决一个服务器服务于多个客户端?现在的代码是处理一个与一个客户端的数据交换,加上循环让他无限制的接收连接的客户端应该便可。
按照msdn的说法TcpListener.Pending 方法 可以确定是否有挂起的连接请求。如果连接正挂起,则为 true;否则为 false。这一非阻止方法将确定是否存在挂起的连接请求。由于 AcceptSocket 和 AcceptTcpClient 方法在 Start 方法将一个传入连接请求排入队列之前一直阻止执行,因此可以使用 Pending 方法确定尝试接受连接之前是否有可用的连接。
前面介绍AcceptTcpClient也提到过,如果希望避免阻止,可以使用 Pending 方法来确定传入连接队列中的连接请求是否可用。
服务器端代码调整:
- TcpListener listener = new TcpListener(IPAddress.Any, 9008);
- listener.Start();
- Console.WriteLine("开始监听...");
- byte[] rcvBuffer = new byte[1024];
- string revStr;
- int revBytes = 0;
- //#newcode
- while (true)
- {
- if (!listener.Pending())//#newcode
- {
- Console.WriteLine("没有连接需要处理!");
- Thread.Sleep(1000);
- continue;
- }
- TcpClient client = listener.AcceptTcpClient(); //从Start中提取排队的客户端
- if (client.Connected)
- {
- Console.WriteLine("客户端已连接,IP:{0}",
- client.Client.RemoteEndPoint.ToString());
- NetworkStream netStream = client.GetStream();
- //向客户端发送欢迎信息
- byte[] welcome = Encoding.Unicode.GetBytes("欢迎光临战国游戏!");
- netStream.Write(welcome, 0, welcome.Length);
- while (true) //连接的客户端持续的接受数据
- {
- //从 NetworkStream 读取数据,如果没有数据返回0
- revBytes = netStream.Read(rcvBuffer, 0, rcvBuffer.Length);
- if (revBytes == 0)
- break;
- revStr = Encoding.Unicode.GetString(rcvBuffer, 0, revBytes);
- Console.WriteLine("接收数据:" + revStr);
- }
- netStream.Close();
- client.Close(); //关闭客户端
- }
- }//#newcode
- listener.Stop(); //停止侦听 (调用不到的代码)
Line 8 为整个程序添加了while循环,让程序可一直循环接收从客户端发来的连接请求。
Line10-15 就是说的Pending是个非阻止方法可以确定是否存在挂起的连接请求。如果没有连接则第隔1秒在Console输出 "没有连接需要不处理!"字样,然后跳过下边的代码重新循环,直到有可用的连接。
其它的代码和以前一样,需要注意的上,加上循环后Line 38
listener.Stop(); //停止侦听
就执行不到了,现在还没停止侦听的操作,如果是GUI的可以使用一个Button来启动与停止侦听。为了使用更少的代码便于理解关键的代码,我们使用Console在里就不好处理了。不过暂时不用理会。知道需要侦听结束就可以了。我们使用手动关闭Console来结束服务吧。
测试#1
@1 启动服务器端
屏幕一直显示没有可用的连接,侦听处于空闲状态。
@ 2 启用客户端1,服务器端显示客户端1的连接IP、端口及接收到的数据。
quit 退出客户端1 程序又回去 @1状态。
在次启动客户端1 回去 @2状态。
依次启动客户端2客户端3
quit 客户端1
客户端2连接上,正常发送数据。
quit 客户端2
客户端3 连接
quit 客户端3 回到@1状态。
测试#1完成
正如前面所说,
AcceptTcpClient(与AcceptSocket) 是阻止方法,与服务器连接上后要等待客户端退出后才能,在从Start中队列中与一下客户端连接,与前面的银行取钱的排队一样。如果有多个客户端连接会被挂起,直到上一个处理完成。
小结:
使用了最简单的方式完成了基本的网络通信。了解了dotNETSocket编程中比较容易使用的几个类与方法。创建基于阻止同步模式的通信的实例。
TcpListener 从 TCP 网络客户端侦听连接。
TcpClient 为 TCP 网络服务提供客户端连接。
AcceptTcpClient 是一个阻止方法,该方法返回可用于发送和接收数据的 TcpClient。如果希望避免阻止,请使用 Pending 方法来确定传入连接队列中的连接请求是否可用。
NetworkStream 类提供在阻止模式下通过 Stream 套接字发送和接收数据的方法。
TcpClient.Connected Connected 属性获取截止到最后一次 I/O 操作时的 Client 套接字的连接状态。如果该属性返回 false,则表明 Client 要么从未连接,要么已断开连接。
掌握了一些网络编程的基本知识,还是要回归到我们的游戏中来,只有通过项目才可以明确技术的需求,用啥学啥呗!
按照阻止同步的通信模式这样接收与发送数据肯定是不行了。总不能让玩家排着队等服务器的处理吧。对于玩家数据的处理应该基本达到实时才行,也就是说不管那个玩家在与服务器通讯过程中响应的时间要尽可能的少。
这个问题通过线程可以很好的解决。将服务器与每个玩家的数据交互放到单独的线程上去处理就不有排队这种现象了。
多线程处理的服务器端
主程序创建侦听,等待客户端的连接。如果有客户端的连接则为客户端创建线程在线程中完成,服务器写客户端的通信。
- class ThreadTcpServer
- {
- TcpListener listener;
- public ThreadTcpServer()
- {
- listener = new TcpListener(IPAddress.Any, 9008);
- listener.Start();
- Console.WriteLine("等待客户端连接...");
- while (true)
- {
- while (!listener.Pending()) //确定是否有挂起的连接请求
- {
- Thread.Sleep(2000);
- }
- ConnectionThread newconnection = new ConnectionThread();
- newconnection.threadListener = this.listener;
- Thread newthread = new Thread(new ThreadStart(newconnection.handle));
- newthread.Start();
- }
- }
- static void Main(string[] args)
- {
- ThreadTcpServer server = new ThreadTcpServer();
-
}
- }
Line 3 定义TcpListener 成员变量。
Line 4-20 在构造函数中创建侦听并等待客户端的连接
Line 17 创建客户端线程
- class ConnectionThread
- {
- public TcpListener threadListener;
- private static int connections = 0;
- public void handle()
- {
- Console.WriteLine("Thread:{0}", Thread.CurrentThread.GetHashCode().ToString());
- byte[] rcvBuffer = new byte[1024];
- string revStr;
- int revBytes = 0;
- TcpClient client = this.threadListener.AcceptTcpClient();
- if (client.Connected)
- {
- connections++;
- Console.WriteLine("客户端已连接,IP:{0},共{1}个连接。",
- client.Client.RemoteEndPoint.ToString(),connections);
- NetworkStream netStream = client.GetStream();
- byte[] welcome = Encoding.Unicode.GetBytes("欢迎光临战国游戏!");
- netStream.Write(welcome, 0, welcome.Length);
- while (true)
- {
- revBytes = netStream.Read(rcvBuffer, 0, rcvBuffer.Length);
- if (revBytes == 0)
- break;
- revStr = Encoding.Unicode.GetString(rcvBuffer, 0, revBytes);
- Console.WriteLine("接收数据:" + revStr);
- }
- connections--;
- netStream.Close();
- client.Close();
- Console.WriteLine("客户端关闭: {0} active connections", connections);
- }
- }
- }
ConnectionThread 是线程处理类,它接收主程序传过的TcpListener在成员函数handle里处理与客户端的连接。
至此我们对dotNET socket 的编程有了一定的认识。