在Linux上编写多线程TCP服务器

6
在工作中,我被要求实现一个TCP服务器作为Modbus从机设备的一部分。我在这里和互联网上(包括优秀的http://beej.us/guide/bgnet/)阅读了很多资料,但我正在处理一个设计问题。简而言之,我的设备只能接受2个连接,在每个连接上都会有传入的Modbus请求,我必须在我的主控制器循环中处理这些请求,然后回复成功或失败状态。我有以下几种实现方式的想法。
  1. 有一个监听线程,创建、绑定、监听和接受连接,然后生成一个新的pthread来监听连接上的传入数据,并在空闲超时期间关闭连接。如果活动线程数目已经是2,新的连接将立即关闭以确保只允许2个连接。
  2. 不从监听线程生成新线程,而是使用select()来检测传入的连接请求以及活动连接上的传入Modbus连接(类似于Beejs指南中的方法)。
  3. 创建2个监听线程,每个线程创建一个套接字(相同的IP和端口号),可以阻塞在accept()调用上,然后关闭套接字fd并处理连接。在这里,我(也许是天真地)假设这将只允许最多2个连接,我可以使用阻塞读来处理它们。
我已经使用C++很长时间了,但我对Linux开发还是比较新手。如果上述哪种方法是最好的(如果有的话),或者我的Linux经验意味着其中任何一个都是非常糟糕的想法,我真的很希望得到任何建议。我渴望避免fork()并坚持pthread,因为传入的Modbus请求将被排队,并定期从主控制器循环中读取。提前感谢您的任何建议。
3个回答

3
第三种方法不可行,因为你只能绑定本地地址一次。
我可能会使用第二种方法,除非你需要进行大量处理,在这种情况下,第一种和第二种方法的组合可能会有用。
我考虑的第一种和第二种方法的组合是:让主线程(程序启动时始终存在的线程)创建两个工作线程,然后执行阻塞的 accept 调用以等待新连接。当一个新连接到达时,告诉其中一个线程开始处理新连接并返回到阻塞状态的 accept。当第二个连接被接受时,告诉另一个线程开始处理该连接。如果两个连接已经打开,则要么不接受直到一个连接关闭,要么等待新连接但立即关闭它们。

我喜欢这个方案的想法,但唯一的问题是我的主循环严格禁止阻塞。它必须进行处理并定期处理来自侦听器线程的请求。考虑到这一点,您认为选项2.最好吗? - mathematician1975
@mathematician1975,您仍然可以使用我的方法,但是不要在accept上阻塞,而是使用短暂或无超时的select(或使侦听套接字非阻塞并使用accept并检查EAGAIN/EWOULDBLOCK)来知道何时可以接受连接。 - Some programmer dude
我认为考虑到我的时间限制,这是我在短期内追求的最佳解决方案。感谢您的建议。 - mathematician1975

2
由于您只处理2个连接,每个连接一个线程对于这种应用程序是完美的。如果您需要扩展到数千个连接,则使用非阻塞或异步I/O的面向对象方法更好。使用2个监听器线程是有意义的,您不需要关闭accept fd。当连接完成时,只需回到它上进行接受即可。实际上,一种变化是有三个线程被阻塞在accept上。如果两个线程正在积极处理连接,则第三个线程将重置新创建的连接(或返回繁忙响应,无论哪种方式适合您的设备)。
要使所有三个线程都在accept上阻塞,您需要在三个线程启动以执行其接受/处理处理之前,由主线程创建和绑定您的套接字。
Linux上的pthread的man页面指示accept是线程安全的。(线程安全函数下面的部分列出了不是线程安全的函数,想想看。)

你是指我问题中的选项3吗? - mathematician1975
@mathematician1975:是的,我在考虑选项3的一个变体,但有三个线程接受。 - jxh
但是这里的另一个答案说我只能绑定一次,这就排除了选项3。 - mathematician1975
@mathematician1975:在主线程中绑定和监听一次,然后启动线程来执行accept操作。Linux支持这种模式,而在其他操作系统上,您可能需要使用互斥锁来保护accept调用。 - jxh
啊,我明白了 - 所以将套接字文件描述符传递给其他线程?那么在任何连接之前,我会有两个线程同时阻塞在accept()上?这是线程安全的吗? - mathematician1975
显示剩余2条评论

2
您提出的所有设计选项都不太面向对象,而且它们更适用于C而不是C ++。如果您的工作允许使用boost,则Boost.Asio库非常适合制作简单(和复杂)的套接字服务器。您可以几乎使用他们的任何示例,并将其轻松扩展为仅允许2个活动连接,一旦打开即关闭所有其他连接。
我想到的是,通过在连接类中保持静态计数器(在构造函数中增加,在析构函数中减少),并在创建新连接时检查计数器并决定是否关闭连接,可以修改他们的简单HTTP服务器以执行此操作。连接类还可以获得boost :: asio :: deadline_timer来跟踪超时。
这最接近您的第一个设计选择,boost可以在1个线程中完成此操作,并在后台执行类似于select()(通常是epoll())的操作。但这是“ C ++ 方法”,我认为使用select()和原始的pthread是C方法。

谢谢您的建议。我完全同意您对C++和面向对象方面的看法。然而,考虑到我目前时间上的限制,我认为我将不得不采用原始的Linux API方法,因为我需要快速推出一个原型,并且已经花费了一些时间来适应它。但是,一旦项目被接受,我认为我肯定会支持这种方法。 - mathematician1975

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接