C#网络编程(三、Socket同步传输字符串)

本文转自:http://blog.csdn.net/shanyongxu/article/details/51189335http://blog.csdn.net/shanyongxu/article/details/51189335,本人学习受益匪浅,楼主把重点都标记出来了,请点击链接查看原文,尊重楼主大大版权。


同步传输字符串

 

接下来考虑着一种情况,完成一个简单的文本通信:

(1).客户端将字符串发送到服务端,服务端接受字符串并显示

(2).服务端将字符串由英文的小写转换为大写,然后发回给客户端,客户端接受并显示.

 

客户端发送,服务端接受并输出

 

1.服务端程序

 

可以在TcpClient上调用GetStream()方法来获得连接到远程计算机的网络流NetworkStream.当在客户端调用时,它获得连接服务端的流;当在服务端调用时,它获得连接客户端的流.

 

先看服务端的代码实现:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
   
using System.Net;  
using System.Net.Sockets;  
namespace Server  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            const int BufferSize = 8192;//缓存大小,8192字节  
   
            Console.WriteLine("Server is running...");  
            IPAddress ip = new IPAddress(new byte[] { 192, 168, 3, 19 });  
            TcpListener listener = new TcpListener(ip,1621);  
   
            listener.Start();//开始监听  
   
            Console.WriteLine("Start Listening...");  
   
            //获取一个连接,中断方法  
            TcpClient remoteClient = listener.AcceptTcpClient();  
   
            //打印连接到的客户端信息  
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);  
   
            //获得流,并写入buffer中  
   
            NetworkStream streamToClient = remoteClient.GetStream();  
            byte[] buffer = new byte[BufferSize];  
            int byteRead = streamToClient.Read(buffer,0,BufferSize);  
   
            //获得请求的字符串  
            string msg = Encoding.Unicode.GetString(buffer,0,byteRead);  
            Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);  
   
            //按Q退出  
            Console.WriteLine("\n\n按Q退出\n\n");  
            ConsoleKey key;  
            do  
            {  
                key = Console.ReadKey(true).Key;  
            } while (key!=ConsoleKey.Q);  
        }  
    }  
}  

等一下,这里楼主有个问题需要问一下,在使用netstat命令查看端口时,你知道根据端口的状态判断哪些端口是可以用来监听的,那些端口不能用来监听吗?

 

这段程序的上半部分上一次说过了,remoteClient.GetStream()方法获取到了连接至客户端的流,然后从流中读出数据并保存在了buffer,随后使用Encoding.Unicode.GetString()方法,从缓存中获取到实际的字符串.最后将字符串显示在了控制台中.这段代码有个地方需要注意:如果能够读取的字符串的总字节数大于BufferSize,就会出现字符串截断现象,只能读取到不完整的字符串.这是因为缓存的字节数是有限的,在本例中是8192.如果传递的数据字节数比较大,例如图片,音频,文件,则必须采用分次读取然后转存”的方式,代码如下:

//获取字符串  
byte[] buffer = new byte[BufferSize];  
int bytesRead;  
MemoryStream ms = new MemoryStream();  
do  
{  
    bytesRead = streamToClient.Read(buffer,0,BufferSize);  
} while (bytesRead>0);  

咱们没有使用”分次读取转存”的方式,为啥呢?因为:楼主的水平有限,不想误人子弟.(关于流,可参考前面的文章。)

 

说实话,8192已经很多了.当使用Unicode编码时,8192字节可以保存4096个汉字和英文字符.使用不同的编码方式,占用的字节数有很大的差异.

 

现在不对客户端在任何修改,先调试运行服务器,在运行客户端,会发现服务端在打印完”Client Connected ! Local:192.168.3.19:1621<-- Client:192.168.3.19:4044”之后,再次被阻塞了,没有继续运行,也没有输出”Reading data,{0} bytes...”等任何字符.可见,AcceptTcpClient()方法类似,Read()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据,执行此方法,否则会一直等待下去.

 

2.客户端程序

 

接下来编写客户端想服务端发送字符串的代码,与服务端类似,先获取连接到服务端的流,将字符串保存到buffer,再将缓存写入流.写入流这一过程,相当于将消息发往服务端.

static void Main(string[] args)  
{  
    #region MyRegion  
    /* 
 
    Console.WriteLine("Client is running..."); 
    TcpClient client; 
    for (int i = 0; i <= 2; i++) 
    { 
        try 
        { 
            client = new TcpClient(); 
            //与服务器建立连接 
            client.Connect(IPAddress.Parse("192.168.3.19"), 1621); 
 
        } 
        catch (Exception ex) 
        { 
            Console.WriteLine(ex.Message); 
            return; 
        } 
        //打印连接到的服务端信息 
        Console.WriteLine("Server Connected ! Local:{0} -->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint); 
    } 
    //按Q退出 
    Console.WriteLine("\n\n输入\"Q\"键退出. "); 
    ConsoleKey key; 
    do 
    { 
        key = Console.ReadKey(true).Key; 
    } while (key != ConsoleKey.Q);*/  
    #endregion  
    Console.WriteLine("Client is running...");  
    TcpClient client;  
    try  
    {  
        client = new TcpClient();  
        //与服务器建立连接  
        client.Connect(IPAddress.Parse("192.168.1.120"),1621);  
    }  
    catch (Exception ex)  
    {  
        Console.WriteLine(ex.Message);  
        return;  
    }  
  
    //打印连接到的服务端信息  
    Console.WriteLine("Server Connected! Local:{0}-->Server:{1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);  
  
    string msg = "Hello,readers!";  
  
    NetworkStream streamToServer = client.GetStream();  
  
    byte[] buffer = Encoding.Unicode.GetBytes(msg);//获得缓存  
    streamToServer.Write(buffer,0,buffer.Length);  
  
    Console.WriteLine("Sent: {0}",msg);  
  
    //按Q退出  
    Console.WriteLine("\n\n按Q退出");  
    ConsoleKey key;  
    do  
    {  
        key = Console.ReadKey(true).Key;  
    } while (key!=ConsoleKey.Q);  
  
}  

现在再次运行程序,得到的输出为:

//服务端:  
Server is running...  
Start Listening...  
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:5465  
Received: Hello,readers! [28bytes]  
//客户端:  
Client is running...  
Server Connected! Local:192.168.1.120:5465-->Server:192.168.1.120:1621  
Sent: Hello,readers!  

 

可以看到,已经成功的发送和接受了一串字符串,是不是有点成就感了?QQ不过如此了了的事.但不要高兴的太早,对于即时通信程序来说,在客户端和服务端之间,应该是可以不间断的接受和发送消息的.但是上面的代码只能接受客户端发送的一条消息,因为代码已经运行完毕,控制外也已经输出了”按Q退出”.无法再继续执行任何的后续动作.此时如果在开启一个客户端,那么出现的情况是:客户端可以与服务器建立连接,也就是”netstat -a”显示为ESTABLISHED,这是操作系统所知道的;但是由于服务端的程序已经执行到了最后一步,只能输入Q退出,无法再执行任何动作.

 

回想一下,前面说过,当一个服务端需要接受多个客户端连接时,所采用的处理办法是:AcceptTcpClient()方法放在一个while循环中.类似的,当服务端需要同一个客户端的多次请求进行处理时,可以将Read()方法也放入到一个do/while循环中.

 

综合起来就只有四种情况:

 

第一种:如果不使用do/while循环,服务端只有一个listener.AcceptTcpClient()方法和一个TcpClient.GetStream().Read()方法,则服务端只能处理来自一个客户端的一条请求.

 

第二种:如果使用一个do/while循环,并将listener.AcceptTcpClient(0方法和TcpClient.GetStream().Read()方法放在循环中,那么服务端将可以处理多个客户端的一条请求.

 

第三种:使用一个do/while循环,并将listener.AcceptTcpClient()方法放在循环内,TcpClient.GetStream().Read()方法放在循环外,那么可以处理一个客户端的多条请求.

 

第四种:使用两个do/while循环,对它们分别进行嵌套,那么结果是啥?你肯定会说,可以处理多个客户端的多个请求.事实上不是这样的.因为内层的do/while循环总是在为一个客户端服务,它会中断在TcpClient.GetStream().Read()方法的位置,而无法执行完毕.即时可以通过某种方式让内层循环退出,例如,当客户端向服务端发送”exit”字符串时,服务端也只能挨个对客户端提供服务.如果服务端想并行的对多个客户端的多个请求进行服务,那么服务端就需要采用多线程.主线程,即执行外层的do/while循环的线程,它在AcceptTcpClient()获取到一个TcpClient之后,必须将内层的do/while循环交给其他的线程去处理,然后主线程快速的重新回到listener.AcceptTcpClient()的位置,来响应其他的客户端?明白了吗?是不是有点晕,楼主也有点晕,没关系.咱们一个一个讲解.

 

咱们先来看第二种和第三种情况.

对于第二种情况,按照上面描述的那些对代码做一些改动,服务端代码:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
   
using System.Net;  
using System.Net.Sockets;  
using System.IO;  
   
namespace Server  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            const int BufferSize = 8192;//缓存大小,8192字节  
   
            Console.WriteLine("Server is running...");  
            IPAddress ip = new IPAddress(new byte[] { 192, 168, 1, 120 });  
            TcpListener listener = new TcpListener(ip,1621);  
   
            listener.Start();//开始监听  
   
            Console.WriteLine("Start Listening...");  
            do  
            {  
                //获取一个连接,中断方法  
                TcpClient remoteClient = listener.AcceptTcpClient();  
                //打印连接到的客户端信息  
                Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);  
   
                //获得流,并写入buffer中  
                NetworkStream streamToClient = remoteClient.GetStream();  
                byte[] buffer = new byte[BufferSize];  
                int bytesRead = streamToClient.Read(buffer,0,BufferSize);  
   
                //获得请求的字符串  
                string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);  
                Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);  
   
            } while (true);  
            /* 
            //获取一个连接,中断方法 
            TcpClient remoteClient = listener.AcceptTcpClient(); 
  
            //打印连接到的客户端信息 
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint); 
  
            //获得流,并写入buffer中 
  
            NetworkStream streamToClient = remoteClient.GetStream(); 
            byte[] buffer = new byte[BufferSize]; 
            int byteRead = streamToClient.Read(buffer,0,BufferSize); 
  
            //获得请求的字符串 
            string msg = Encoding.Unicode.GetString(buffer,0,byteRead); 
            Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead); 
  
            //按Q退出 
            Console.WriteLine("\n\n按Q退出\n\n"); 
            ConsoleKey key; 
            do 
            { 
                key = Console.ReadKey(true).Key; 
            } while (key!=ConsoleKey.Q); 
            */  
           /* //获取字符串 
            byte buffer = new byte[BufferSize]; 
            int bytesRead; 
            MemoryStream ms = new MemoryStream(); 
            do 
            { 
                bytesRead = streamToClient.Read(buffer,0,BufferSize); 
            } while (bytesRead>0);*/  
        }  
    }  
}  

然后启动多个客户端程序,在服务端可以看到这样的情况:

Server is running...  
Start Listening...  
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6737  
Received: Hello,readers! [28bytes]  
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6742  
Received: Hello,readers! [28bytes]  
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6754  
Received: Hello,readers! [28bytes]  

 

现在将第二种情况变为第三种情况,只需要将do向下挪动几行就可以了:

//获取一个连接,中断方法  
TcpClient remoteClient = listener.AcceptTcpClient();  
//打印连接到的客户端信息  
Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);  
//获得流,并写入buffer中  
NetworkStream streamToClient = remoteClient.GetStream();  
do  
{                  
    byte[] buffer = new byte[BufferSize];  
    int bytesRead = streamToClient.Read(buffer, 0, BufferSize);  
  
    //获得请求的字符串  
    string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);  
    Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);  
  
} while (true);  

然后再改动一下客户端,让它可以连续发送多个字符串到到服务器.当按下回车的时候发送字符串,输入”Q”的时候,跳出循环:

Console.WriteLine("Client is running...");  
TcpClient client;  
try  
{  
    client = new TcpClient();  
    //与服务器建立连接  
    client.Connect(IPAddress.Parse("192.168.1.120"), 1621);  
}  
catch (Exception ex)  
{  
    Console.WriteLine(ex.Message);  
    return;  
}  
  
//打印连接到的服务端信息  
Console.WriteLine("Server Connected! Local:{0}-->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);  
  
  
  
NetworkStream streamToServer = client.GetStream();  
  
string msg;  
do  
{  
    Console.Write("Sent:");  
    msg = Console.ReadLine();  
    if (!String.IsNullOrEmpty(msg) && msg != "Q")  
    {  
        byte[] buffer = Encoding.Unicode.GetBytes(msg);  
        try  
        {  
            //发往服务器  
            streamToServer.Write(buffer, 0, buffer.Length);  
        }  
        catch (Exception ex)  
        {  
            Console.WriteLine(ex.Message);  
            return;  
        }  
    }  
} while (msg != "Q");  

接下来先运行服务端,再运行客户端,输入以下字符串来进行测试,应该能够看到预期的结果.

 

注意:如果再开启一个客户端,该客户端虽然可以成功的连接服务端,也可以发送字符串,但是服务端不会做任何的处理和显示.

 

这里有一点需要注意:当客户端在TcpClient实例上调用Close()方法,或者在流上调用Didpose()方法时,服务端的streamToClient.Read()方法会持续返回0,但是不抛出异常,所以会产生一个无限循环.服务端不断的刷新显示”Received: [0 bytes]”,因此,服务端在调用streamToClient.Read()方法后,应加上一个如下判断:

int bytesRead = streamToClient.Read(buffer, 0, BufferSize);  
if (bytesRead==0)  
{  
    Console.WriteLine("Client offline");  
    break;  
}  

如果直接关闭掉客户端,或者客户端执行完毕但没有调用stream.Dispose()或者TcpClient.Close(),切服务端此时仍阻塞在Read()方法处,则会在服务端抛出异常:未经处理的异常:  System.IO.IOException: 无法从传输连接中读取数据:远程主机强迫关闭了一个现有的连接。

 

因此,服务端的streamToClient.Read()方法需要写在一个try/catch.下面是改进的服务端代码:

do  
{  
    try  
    {  
        byte[] buffer = new byte[BufferSize];  
        int bytesRead = streamToClient.Read(buffer, 0, BufferSize);  
        if (bytesRead == 0)  
        {  
            Console.WriteLine("Client offline");  
            break;  
        }  
        //获得请求的字符串  
        string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);  
        Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);  
    }  
    catch (Exception ex)  
    {  
        Console.WriteLine(ex.Message);  
        break;  
    }  
} while (true);  

同样的,如果服务端在连接到客户端之后调用remoteClient.Close(),则客户端在调用streamToServer.Write()时也会抛出异常.因此,他们的读写操作都必须放入try/catch块中.

 

 

服务端发送,客户端接受并显示

 

1.服务端程序

 

到现在为止,客户端已经能发送字符串到服务端,服务端能接受并显示.接下来,再进行进一步处理,使服务端将字符串有英文小写改为英文大写,然后发回给客户端,客户端接受并显示.此时服务端和客户端的角色和上面进行了一下对调:对于服务端来说,就好像刚才的客户端一样,将字符串写入到流中,而客户端则同服务端一样,接受并显示.

 

服务端:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using System.Net;  
using System.Net.Sockets;  
using System.IO;  
   
namespace Server  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            const int BufferSize = 8192;//缓存大小,8192字节  
   
            Console.WriteLine("Server is running...");  
            IPAddress ip = new IPAddress(new byte[] { 192,168,1,120});  
   
            TcpListener listener = new TcpListener(ip, 1621);  
            //开始监听  
            listener.Start();  
            Console.WriteLine("Start Listening...");  
   
            //获取一个连接,中断方法  
            TcpClient remoteClient = listener.AcceptTcpClient();  
   
            //打印连接到的客户端信息  
            Console.WriteLine("Client Connected! Local:{0}<--Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);  
   
            //获得流  
            NetworkStream streamToClient = remoteClient.GetStream();  
   
            do  
            {  
                try  
                {  
                    byte[] buffer = new byte[BufferSize];  
                    int bytesRead = streamToClient.Read(buffer,0,BufferSize);  
                    if (bytesRead==0)  
                    {  
                        Console.WriteLine("Client offline");  
                        break;  
                    }  
                    //获得请求的字符串  
                    string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);  
                    Console.WriteLine("Received: {0} [{1} bytes]",msg,bytesRead);  
   
                    //转换为大写  
   
                    msg = msg.ToUpper();  
                    buffer = Encoding.Unicode.GetBytes(msg);  
                    streamToClient.Write(buffer,0,buffer.Length);  
   
                    Console.WriteLine("Sent: {0}",msg);  
                }  
                catch (Exception ex)  
                {  
                    Console.WriteLine(ex.Message);  
                    break;  
                }  
            } while (true);  
            streamToClient.Dispose();  
            remoteClient.Close();  
   
            //按Q退出  
            Console.WriteLine("\n\n按Q退出");  
            ConsoleKey key;  
            do  
            {  
                key = Console.ReadKey(true).Key;  
            } while (key!=ConsoleKey.Q);  
        }  
    }  
}  

上面的代码大家应该很熟悉了,主要的变化是转换字母大小写,并写入到流中的操作.

 

2.客户端代码  

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Net;  
using System.Net.Sockets;  
using System.Text;  
using System.Threading.Tasks;  
   
namespace Client  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            Console.WriteLine("CLient is running...");  
            TcpClient client;  
            const int BufferSize = 8192;  
   
            try  
            {  
                client = new TcpClient();  
                //与服务器建立连接  
                client.Connect(IPAddress.Parse("192.168.3.19"), 9322);  
            }  
            catch (Exception ex)  
            {  
                Console.WriteLine(ex.Message);  
                return;  
            }             
            //打印连接到的服务端信息  
            Console.WriteLine("Server Connected! Local: {0} --> Server: {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);  
   
            NetworkStream streamToServer = client.GetStream();  
   
            string msg;  
   
            do  
            {  
                Console.Write("Sent:");  
                msg = Console.ReadLine();  
   
                if (!string.IsNullOrEmpty(msg)&&msg!="Q")  
                {  
                    byte[] buffer = Encoding.Unicode.GetBytes(msg);  
                    try  
                    {  
                        //发往服务器  
                        streamToServer.Write(buffer,0,buffer.Length);  
                        int bytesRead;  
                        buffer = new byte[BufferSize];  
   
                        //接受并显示服务器回传的字符串  
                        bytesRead = streamToServer.Read(buffer,0,BufferSize);  
   
                        if (bytesRead==0)  
                        {  
                            Console.WriteLine("Server offline");  
                            break;  
                        }  
                        msg = Encoding.Unicode.GetString(buffer,0,bytesRead);  
                        Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);  
                    }  
                    catch (Exception ex)  
                    {  
                        Console.WriteLine(ex.Message);  
                        break;  
                    }  
                }  
            } while (msg!="Q");  
            streamToServer.Dispose();  
            client.Close();  
        }  
    }  
}  

先运行一下服务端,然后运行客户端,就会看到相应的输出.

 

分享一下demo:(Unity项目,使用需要改动代码里ip地址)http://pan.baidu.com/s/1i5omGtR

这样大家应该会对C#的网络编程有了一定得了解,当然了,这是只是网络编程中的皮毛.因为到目前为止,咱们的操作都是同步操作,上面的代码只能作为入门使用.在实际中,一个服务端只能为一个客户端提供服务的情况几乎不存在.

 

下面咱们要看异步的网络编程之前,先学习一下在不同的编码方式中英文的大小,以及TCP缓存导致的文本边界问题.


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值