ROS交流群
ROS Group
产品服务
Product Service
开源代码库
Github
官网
Official website
技术交流
Technological exchanges
激光雷达
LIDAR
ROS教程
ROS Tourials
深度学习
Deep Learning
机器视觉
Computer Vision

如何正确的处理TCP连接



  • TCP是一种非常常用的socket,在做各种网络通信的时候,它也是必不可少的。但是很多情况下我们并没有正确的处理TCP连接。最终可能导致大量的僵尸连接耗尽了服务器的资源。这里就稍微详细的说明正确的TCP连接创建和关闭的方式,以及僵尸连接出现的原因。因为作为开发软件的人更关心的是如何正确的使用API,所以底层的和如何使用关系不大的东西,比如TCP三次握手,在这篇文章会尽量避免。

    以下以C#作为示例

    建立连接

    服务器开始监听

    IPAddress ip = IPAddress.Parse ("0.0.0.0");
    var serverSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    serverSocket.Bind (new IPEndPoint (ip, Convert.ToInt32 (9999)));
    serverSocket.Listen (1000);
    Socket clientSocket = serverSocket.Accept ();
    

    一般设置好服务器监听地址和端口,服务器就可以开始监听了。这里采用的是阻塞式的socket。程序最后会阻塞在最后一句直到有客户端连接到服务器。

    客户端建立连接

    IPAddress targetIp = IPAddress.Parse (ipstr);
    Socket mClientSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    mClientSocket.Connect (new IPEndPoint (targetIp, Convert.ToInt32 (9999)));
    

    客户端程序也是比较简单的,设置好服务器的地址和端口就可以连接了。这里socket的读取和写入都是阻塞的。所以一般都是要单独给每个socket连接开一个线程。

    创建连接的过程是很直观和简单的,一般也很少会犯错。但是socket的断开过程就是一个比较大的坑。

    不正确的断开连接

    如果服务器端或者客户端想要关闭连接,直接调用了Close方法。那么就会出问题。如服务器端程序执行

    mClientSocket.Close ();
    

    此时如果客户端正在调用 Receive 函数,客户端就会报出Socket被异常关闭的错误。但是客户端的这个socket实际上处在close_wait状态并没有关闭,socket也并不会被释放。如果程序这样一直执行下去最终就会耗尽系统的资源

    下面以实际的一个例子程序来演示一下

    服务器程序

    using System;
    using System.Net;
    using System.Net.Sockets;
    
    namespace testSocket
    {
    	class MainClass
    	{
    		public static void Main (string[] args)
    		{
    			
    			IPAddress ip = IPAddress.Parse ("0.0.0.0");
    			var serverSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    			serverSocket.Bind (new IPEndPoint (ip, Convert.ToInt32 (9999)));
    			serverSocket.Listen (1000);
    			Socket clientSocket = serverSocket.Accept ();
    			clientSocket.Close ();
    			Console.WriteLine ("Socket connected");
    			Console.ReadKey ();
    		}
    	}
    }
    
    

    客户端程序

    using System;
    using System.Net;
    using System.Net.Sockets;
    
    namespace socketClient
    {
    	class MainClass
    	{
    		public static void Main (string[] args)
    		{
    			IPAddress targetIp = IPAddress.Parse ("127.0.0.1");
    			Socket mClientSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    			mClientSocket.Connect (new IPEndPoint (targetIp, Convert.ToInt32 (9999)));
    			Console.WriteLine ("Socket connected");
    			Console.WriteLine ("Local Port: " + mClientSocket.LocalEndPoint);
    			Console.ReadKey ();
    		}
    	}
    }
    
    

    服务器程序在连接建立之后直接调用了Close函数。
    这时通过netstat可以看到两个连接的状态。
    在命令行执行

    netstat -nap |grep 46755
    

    其中46755是客户端显示的端口号
    显示如下图
    0_1466499938307_Screenshot from 2016-06-21 17:05:11.png
    可以看到服务器也有一个socket处在FIN_WAIT2的状态。但是这个并没有什么影响,过一段时间后系统会自动释放这个socket。
    0_1466500313131_Screenshot from 2016-06-21 17:11:38.png
    同样如果客户端直接关闭连接就会导致服务器的socket处在close_wait的状态。

    正确的断开连接

    关闭之前一定要调用Shutdown
    关闭之前一定要调用Shutdown
    关闭之前一定要调用Shutdown
    重要的事情说三遍。Shutdown的作用就是告诉socket不要在继续读写了,socket要准备关闭了。Shutdown的方式有几种,可以只关闭本地的读写,也可以只关闭远方的读写,也可以同时关闭。

    正确的服务器关闭例子

    using System;
    using System.Net;
    using System.Net.Sockets;
    
    namespace testSocket
    {
    	class MainClass
    	{
    		public static void Main (string[] args)
    		{
    			bool closeFlag = false;
    			IPAddress ip = IPAddress.Parse ("0.0.0.0");
    			var serverSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    			serverSocket.Bind (new IPEndPoint (ip, Convert.ToInt32 (9999)));
    			serverSocket.Listen (1000);
                            byte[] buf = new byte[1024 * 512];
    			Socket clientSocket = serverSocket.Accept ();
                            try{
                                int size = clientSocket.Receive(buf);
                                if(size == 0 && !closeFlag){
                                    closeFlag = true;
                                    clientSocket.Shutdown();
                                    clientSocket.Close();
                                }
                            }catch(SocketException e){
                                closeFlag = true;
                                clientSocket.Shutdown();
                                clientSocket.Close();
                            }
                            
                            if(!closeFlag){
                                clientSocket.Shutdown (SocketShutdown.Both);
        			    clientSocket.Close ();
                            }
    			Console.WriteLine ("Socket connected");
    			Console.ReadKey ();
    		}
    	}
    }
    

    不仅在本地主动关闭的时候要调用Shutdown,对方不正常关闭的时候也要调用Shutdown。如果对方不正常的关闭连接那么就会本地的socket就会抛出异常。如果对方是正常的关闭socket那么Receive的返回值就是0,然后根据这个去关闭本地的socket。

    比较完整的C#socket客户端和服务器的例子程序可以看这里。
    服务器程序
    客户端程序