草庐IT

C# Socket/TCPClient断线重连/不断重连的简单思路+代码,海量注释

羅漢果茶 2023-08-27 原文

前言


最近在写一个透传项目,需要实现一个TCPClient模式的透传。在没有连接上时会去不断发起连接直至连接成功, 还有断连后又会不断发起请求连接,直至再次连接成功。 作为小白,第一反应就是去百度,结果百度搜索出来的,全是CSDN,而且清一色都是上来贴一大堆代码,令人头晕,还一大堆重复的,越看越烦而且搜索无果。

既然没有路,那就由我自己来开辟!


连接成功前进行不断发起请求连接

其实这个功能思路非常简单,无非就是 尝试连接=>连接失败=>重连(连接成功就跳出)。

用代码写出来:

//创建一个新的Socket对象
Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp)
try
{
    client.Connect(IPAddress.Parse(IP地址), 端口号);//尝试连接
}
catch
{
    client.Close();//先关闭
    /*使用新的客户端资源覆盖,上一个已经废弃。如果继续使用以前的资源进行连接,即使参数正确,
    服务器全部打开也会无法连接*/
    client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
    client.Connect(IPAddress.Parse(IP地址), 端口号);//尝试连接
}

复制代码

而这上面仅是进行一次失败重连,如果再失败怎么办?所以我们要不断重复这个步骤。加一层while循环让它不断进行重连。 代码如下:

//创建一个新的Socket对象
Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp)
while(true)//无限循环
{
    try
    {
        client.Connect(IPAddress.Parse(IP地址), 端口号);//尝试连接,失败则会跳去catch
        break;//在此处加上break,成功就跳出循环,避免死循环
    }
    catch
    {
        client.Close();//先关闭
        /*使用新的客户端资源覆盖,上一个已经废弃。如果继续使用以前的资源进行连接,即使参数正确,
        服务器全部打开也会无法连接*/
        client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
    }
}

复制代码

其实这么写,就已经实现了我们初步的功能,客户端会进行不断地连接,直至成功连上服务器。但是,这里有个很严重的问题,如果一直没连上,一直在执行这一步重连,程序会卡死在这里。 所以我们需要额外多开个子线程去执行这一步操作。

//创建一个新的Socket对象
Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp)
//将方法写进线程中
Thread thread=new Thread(() =>
{
    while(true)//无限循环
    {
        try
        {
            client.Connect(IPAddress.Parse(IP地址), 端口号);//尝试连接,失败则会跳去catch
            break;//在此处加上break,成功就跳出循环,避免死循环
        }
        catch
        {
            client.Close();//先关闭
            /*使用新的客户端资源覆盖,上一个已经废弃。如果继续使用以前的资源进行连接,
            即使参数正确, 服务器全部打开也会无法连接*/
            client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
            Thread.Sleep(1000);//等待1s再去重连
        }
    }
});
thread.IsBackground = true;//设置为后台线程,在程序退出时自己会自动释放
thread.Start();//开始执行线程
复制代码

即使一直连不上也不会使程序卡死,会一直进行重连直到连上服务器。但是我们的问题还没解决。那就是 循环结束问题。这部分尝试不断连接,while循环的条件是true,无限循环,就会导致即使连上后,虽然break,但是线程没有结束,还是会继续去进行无限循环。我们需要重新设置循环中止条件。

代码如下:

//创建一个新的Socket对象
Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp)
public static bool IsConnet=true;//判断是否成功连接,设置为全局变量,方便随时控制
//将方法写进线程中
Thread thread=new Thread(() =>
{
    while(IsConnet)//循环
    {
        try
        {
            client.Connect(IPAddress.Parse(IP地址), 端口号);//尝试连接,失败则会跳去catch
            IsConnet=false;//成功连接后修改bool值为false,这样下一步循环就不再执行。
            break;//在此处加上break,成功就跳出循环,避免死循环
        }
        catch
        {
            client.Close();//先关闭
            /*使用新的客户端资源覆盖,上一个已经废弃。如果继续使用以前的资源进行连接,
            即使参数正确, 服务器全部打开也会无法连接*/
            client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
            Thread.Sleep(1000);//等待1s再去重连
        }
    }
});
thread.IsBackground = true;//设置为后台线程,在程序退出时自己会自动释放
thread.Start();//开始执行线程
复制代码

这样就不会再有无限循环的问题了。至此,我们已经完成了功能的一半,这部分已经实现了连接前的不断请求连接,那么连接后又断开呢?很显然,如果在连接后再断开,我们无法进行重连。
另一半就要实现 断线重连。


断线重连

这个思路同样很简单,就是服务器断开->调用连接方法->不断连接
连接方法就是我们上一步写过的功能,我们已经实现不断连接了,我们要将上一步的功能封装成一个方法体去调用就可以了。
连接上服务器后,就是个不断接收的过程,所以也需要多开一个线程去不断接收消息。 代码如下:

//注意,这里的开始部分还是上一步的代码,只不过嵌进了方法体
public void Connet(string Iptxt,int Port)//接收参数是目标ip地址和目标端口号。客户端无须关心本地端口号
{
    //创建一个新的Socket对象
    Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp)
    IsConnet=true;//注意,此处是全局变量,将其设置为true
    //将方法写进线程中
    Thread thread=new Thread(() =>
    {
        while(IsConnet)//循环
        {
            try
            {
                client.Connect(IPAddress.Parse(Iptxt), Port);//尝试连接,失败则会跳去catch
                IsConnet=false;//成功连接后修改bool值为false,这样下一步循环就不再执行。
                break;//在此处加上break,成功就跳出循环,避免死循环
            }
            catch
            {
                client.Close();//先关闭
                /*使用新的客户端资源覆盖,上一个已经废弃。如果继续使用以前的资源进行连接,
                即使参数正确, 服务器全部打开也会无法连接*/
                client=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
                Thread.Sleep(1000);//等待1s再去重连
            }
        }
        /*这里不一样就是放接收线程,在连接上后break出来,执行。
        因为需要带参数,所以要用到特别的ParameterizedThreadStart,
        然后开始线程。↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓*/
        Thread thread2 = new Thread(new ParameterizedThreadStart(ClientReceiveData));//接收线程方法
        thread2.IsBackground = true;//该值指示某个线程是否为后台线程。
        thread2.Start(client);//参数是用我们自建的Socket对象,就是上面的Socket client=new……
        
    });
    thread.IsBackground = true;//设置为后台线程,在程序退出时自己会自动释放
    thread.Start();//开始执行线程
}
复制代码

这样就是在连接后就会进入不断接收的子线程,接下来写接收程序的代码,与网络上的大同小异,只不过我们会稍作改动,在异常断开和正常退出时都去重新进行连接。Ip地址和端口号可以设置成全局变量,方便进行获取,代码如下:

 public void ClientReceiveData(object socket)//TCPClient消息的方法
{
    var ProxSocket = socket as Socket;//处理上一步传过来的Socket函数
    byte[] data = new byte[1024 * 1024];//接收消息的缓冲区
    while (!IsConnet)//同样循环中止的条件
    {
        int len = 0;//记录消息长度,以及判断是否连接
        try
        {
            //连接函数Receive会将数据放入data,从0开始放,之后返回数据长度。
            len = ProxSocket.Receive(data, 0, data.Length, SocketFlags.None);
        }
        catch (Exception)
        {
            //异常退出
            ProxSocet.ShutDown(SocketShutdown.Both);//中止传输
            ProxSocet.Close();//关闭
            Connet(ip地址,端口号);//重新尝试去连接
            IsConnet=false;//注意,此处是全局变量,将其设置为false,防止循环
            return;//让方法结束,终结当前接收服务端数据的异步线程
        }
        if (len <= 0)
        {
            //如果小于0,证明无连接,服务端正常退出
            ProxSocet.ShutDown(SocketShutdown.Both);//中止传输
            ProxSocet.Close();//关闭
            Connet(ip地址,端口号);//重新尝试去连接
            IsConnet=false;//注意,此处是全局变量,将其设置为false,防止循环
            return;//让方法结束,终结当前接收服务端数据的异步线程
        }
        //这里做你想要对消息做的处理
        //string str = Encoding.Default.GetString(data, 0, len);//二进制数组转换成字符串……
    }
}
复制代码

到这里就已经全部实现了!!接下来看看效果吧!!(以本人做的项目做例子)。

本次分享结束,有什么不足的地方,希望大家可以指出,或者不懂的可以留言问我,我们可以多交流!如果想实现图里动态刷新连接状态,可以看我上一篇文章C#LINQ实现动态刷新

2022.11/25 BUG反映


有些小伙伴会反映有以下BUG:

ProxSocet.Shutdown(SocketShutdown.Both);
---------------------
System.Net.Sockets.SocketException:“由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接收数据的请求没有被接受。”

按照这位朋友说的改正就好了!

 

之前写文章的时候,是直接根据自己已经成功实现的思路去白板徒手写了代码去复现,没有做测试,所以有了意想不到的BUG。感谢各位网友,你们都很强大!

有关C# Socket/TCPClient断线重连/不断重连的简单思路+代码,海量注释的更多相关文章

  1. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  2. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  3. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  4. ruby - 简单获取法拉第超时 - 2

    有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url

  5. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  6. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  7. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  8. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  9. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  10. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

随机推荐