DDZZ669 发表于 2022-9-27 22:04

Linux网络编程-TCP客户端如何获取要连接的服务端IP?

<div class='showpostmsg'><h1 cid="n0" mdtype="heading">1 问题引出</h1>

<p cid="n2" mdtype="paragraph">在进行socket通信开发时,一般会用到TCP或UDP这两种传输层协议,<strong>UDP(User Datagram Protocol)是一种面向无连接的协议</strong>,在数据发送前,不需要提前建立连接,它可以更高效地传输数据,但可靠性无法保证。<strong>TCP(Transmission Control Protocol)是一种面向连接的协议</strong>,一个应用程序开始向另一个应用程序发送数据之前,必须先进行握手连接,以保证数据的可靠传输。所以,对于数据可靠性要求较高的场合,一般使用TCP协议通信。</p>

<p cid="n3" mdtype="paragraph"></p>

<p cid="n4" mdtype="paragraph">在<strong>使用TCP方式的socket编程,客户端需要知道服务端的IP和端口号</strong>,然后向服务端申请连接,<strong>对于端口号,可以事先固定一个特定的端口号,但对于IP地址</strong>,在实际的开发使用中,比如嵌入式开发中,两个连网的硬件需要进行TCP通信,<strong>在建立通信,客户端硬件是不知道服务端硬件IP的</strong>(除了程序开发阶段,事先知道IP,将IP写死到程序中),因为通常情况下IP是由路由器分配的,不是一个固定值,这种情况,<strong>客户端如何自动获取服务端的IP来建立TCP通信呢?</strong></p>

<p cid="n5" mdtype="paragraph"></p>

<h1 cid="n6" mdtype="heading">2 解决方案</h1>

<p cid="n7" mdtype="paragraph">本篇就来实现一种解决方法:<strong>在建立TCP通信前,可以先通过UDP通信来获取服务端的IP</strong>。</p>

<blockquote cid="n8" mdtype="blockquote">
<p cid="n9" mdtype="paragraph"><strong>UDP具有广播功能</strong>,客户端可以通过UDP广播,向局域网内的所有设置发送广播包,可以事先定义一种广播协议,服务端在收到特定的广播包后,判断为有客户端需要请求连接,则将自己的IP地址发送出去,当客户端收到服务端发出的IP信息后,即可通过解析到的服务端IP地址,实现与服务端进行TCP连接。</p>
</blockquote>

<p cid="n10" mdtype="paragraph"></p>

<h1 cid="n11" mdtype="heading">3 编程实现</h1>

<p cid="n12" mdtype="paragraph">在进行客户端与服务端的socket编程之前,先实现一些两个程序都会用到的功能代码。</p>

<h2 cid="n13" mdtype="heading">3.1 公共代码块</h2>

<p cid="n14" mdtype="paragraph">服务端要将自己的IP发给客户端,首先要能自动获取到自己的IP,客户端在进行UDP广播时,也可以将自己的IP也一起发出去作为附加信息,所以,需要先实现一个获取自己IP地址的函数:</p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
#define ETH_NAME &quot;wlan0&quot;
//获取本机ip(根据实际情况修改ETH_NAME)
bool get_local_ip(std::string &amp;ip)
{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp;int sock = socket(AF_INET, SOCK_DGRAM, 0);
&nbsp; &nbsp;if (sock == -1)
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] socket err!\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp;} &nbsp;

&nbsp; &nbsp;struct ifreq ifr;
&nbsp; &nbsp;memcpy(&amp;ifr.ifr_name, ETH_NAME, IFNAMSIZ);
&nbsp; &nbsp;ifr.ifr_name = 0;
&nbsp; &nbsp;if (ioctl(sock, SIOCGIFADDR, &amp;ifr) &lt; 0)
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] ioctl err!\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp;} &nbsp;

&nbsp; &nbsp;struct sockaddr_in sin;
&nbsp; &nbsp;memcpy(&amp;sin, &amp;ifr.ifr_addr, sizeof(sin));
&nbsp; &nbsp;ip = std::string(inet_ntoa(sin.sin_addr));
&nbsp; &nbsp;return true;
}</pre>

<p cid="n16" mdtype="paragraph">在进行UDP广播时,客户端与服务端需要事先规定一种信息格式,当格式符合时,说明是客户端要请求IP信息,以及服务端返回的IP信息,本篇的测试程序,规定一种比较简单的方式:</p>

<blockquote cid="n17" mdtype="blockquote">
<ul cid="n18" data-mark="-" mdtype="list">
        <li cid="n19" mdtype="list_item">
        <p cid="n20" mdtype="paragraph">客户端请求服务端IP的信息格式为:字符串&quot;new_client_ip&quot;+分隔符&ldquo;:&rdquo;+客户端自己的IP</p>
        </li>
        <li cid="n21" mdtype="list_item">
        <p cid="n22" mdtype="paragraph">服务端回复自己的IP的信息格式为:字符串&quot;server_ip&quot;+分隔符&ldquo;:&rdquo;+服务端自己的IP</p>
        </li>
</ul>
</blockquote>

<p cid="n23" mdtype="paragraph">因为这里的信息是字符串,并以冒号分割符来分隔信息段,因此,需要先编写一个能拆分字符串的函数:</p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
#define REQUEST_INFO &quot;new_client_ip&quot; //客户端发送的广播信息头
#define REPLAY_INFO &quot;server_ip&quot; //服务端回复的信息头
#define INFO_SPLIT std::string(&quot;:&quot;) //信息分割符

//对c字符串按照指定分割符拆分为多个string字符串
void cstr_split(char *cstr, vector&lt;std::string&gt; &amp;res, std::string split = INFO_SPLIT)
{ &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp;res.clear();
&nbsp; &nbsp;char *token = strtok(cstr, split.c_str());
&nbsp; &nbsp;while(token)
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;res.push_back(std::string(token));
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] token:%s\n&quot;, __func__, token);
&nbsp; &nbsp; &nbsp; &nbsp;token = strtok(NULL, split.c_str());
&nbsp;}
}

//---------使用示例: 解析服务器的ip----------
char recvbuf={0};
//...接收服务端返回的信息
vector&lt;std::string&gt; recvInfo;
cstr_split(recvbuf, recvInfo);
if(recvInfo.size() == 2 &amp;&amp; recvInfo == REPLAY_INFO)
{
&nbsp; &nbsp;std::string serverIP = recvInfo;
//...后续处理</pre>

<p cid="n25" mdtype="paragraph">在进行UDP广播前,需要先设置该套接字为广播类型,这里将此部分代码封装为一个函数</p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
//设置该套接字为广播类型 &nbsp;
void set_sockopt_broadcast(int socket, bool bEnable = true)
{
&nbsp; &nbsp;const int opt = (int)bEnable; &nbsp;
&nbsp; &nbsp;int nb = setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (char *)&amp;opt, sizeof(opt)); &nbsp;
&nbsp; &nbsp;if(nb == -1) &nbsp;
&nbsp;{ &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] set socket error\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp;return; &nbsp;
&nbsp;} &nbsp;
}</pre>

<h2 cid="n27" mdtype="heading">3.2 客户端程序</h2>

<h3 cid="n28" mdtype="heading">3.2.1 客户端进行UDP广播</h3>

<p cid="n29" mdtype="paragraph"><strong>客户端进行UDP广播的主要逻辑是:</strong></p>

<ul cid="n30" data-mark="-" mdtype="list">
        <li cid="n31" mdtype="list_item">
        <p cid="n32" mdtype="paragraph">获取自己的IP(作为UDP广播的附加信息)</p>
        </li>
        <li cid="n33" mdtype="list_item">
        <p cid="n34" mdtype="paragraph">创建一个socket,类型为UDP数据报(SOCK_DGRAM)</p>
        </li>
        <li cid="n35" mdtype="list_item">
        <p cid="n36" mdtype="paragraph">sockaddrd的IP设置为广播IP(INADDR_BROADCAST, 255.255.255.255)</p>
        </li>
        <li cid="n37" mdtype="list_item">
        <p cid="n38" mdtype="paragraph">为socket添加广播属性(setsockopt,SO_BROADCAST)</p>
        </li>
        <li cid="n39" mdtype="list_item">
        <p cid="n40" mdtype="paragraph">发送UDP广播报(sendto)</p>
        </li>
        <li cid="n41" mdtype="list_item">
        <p cid="n42" mdtype="paragraph">接收UDP回复信息(recvfrom),接收设置超时时间(setsockopt,SO_RCVTIMEO),没收到服务端回复则继续广播</p>
        </li>
        <li cid="n43" mdtype="list_item">
        <p cid="n44" mdtype="paragraph">收到服务端回复后,解析出服务端的IP地址,然后即可中止广播</p>
        </li>
</ul>

<p cid="n45" mdtype="paragraph"><strong>具体代码实现如下:</strong></p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
int main() &nbsp;
{ &nbsp;
&nbsp; &nbsp;bool bHasGetServerIP = false;
&nbsp; &nbsp;thread th_tcp_client;

&nbsp; &nbsp;std::string localIP = &quot;xxx&quot;;
&nbsp; &nbsp;if (true == get_local_ip(localIP))
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] localIP: [%s] %s\n&quot;, __func__, ETH_NAME, localIP.c_str());
&nbsp;}

&nbsp; &nbsp;int udpClientSocket = -1; &nbsp;
&nbsp; &nbsp;if ((udpClientSocket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) &nbsp;
&nbsp;{ &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] socket error\n&quot;, __func__); &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;return false; &nbsp;
&nbsp;} &nbsp; &nbsp;

&nbsp; &nbsp;struct sockaddr_in udpClientAddr; &nbsp;
&nbsp; &nbsp;memset(&amp;udpClientAddr, 0, sizeof(struct sockaddr_in)); &nbsp;
&nbsp; &nbsp;udpClientAddr.sin_family=AF_INET; &nbsp;
&nbsp; &nbsp;udpClientAddr.sin_addr.s_addr=htonl(INADDR_BROADCAST); &nbsp;
&nbsp; &nbsp;udpClientAddr.sin_port=htons(6000); &nbsp;
&nbsp; &nbsp;int nlen=sizeof(udpClientAddr); &nbsp;
&nbsp; &nbsp;
&nbsp; &nbsp;set_sockopt_broadcast(udpClientSocket);

&nbsp; &nbsp;while(1) &nbsp;
&nbsp;{ &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;if(bHasGetServerIP)
&nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;continue; //获取到服务器的IP后, 就不需要再广播了
&nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;//从广播地址发送消息 &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;std::string smsg = REQUEST_INFO + INFO_SPLIT + localIP;
&nbsp; &nbsp; &nbsp; &nbsp;int ret=sendto(udpClientSocket, smsg.c_str(), smsg.length(), 0, (sockaddr*)&amp;udpClientAddr, nlen); &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;if(ret&lt;0) &nbsp;
&nbsp; &nbsp; &nbsp;{ &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] sendto error, ret: %d\n&quot;, __func__, ret); &nbsp;
&nbsp; &nbsp; &nbsp;} &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;else &nbsp;
&nbsp; &nbsp; &nbsp;{ &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] broadcast ok, msg: %s\n&quot;, __func__, smsg.c_str()); &nbsp;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/* 设置阻塞超时 */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;struct timeval timeOut;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;timeOut.tv_sec = 2; //设置2s超时
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;timeOut.tv_usec = 0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (setsockopt(udpClientSocket, SOL_SOCKET, SO_RCVTIMEO, &amp;timeOut, sizeof(timeOut)) &lt; 0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] time out setting failed\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return 0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//再接收数据
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;char recvbuf={0};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int num = recvfrom(udpClientSocket, recvbuf, 100, 0, (struct sockaddr*)&amp;udpClientAddr,(socklen_t*)&amp;nlen);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (num &gt; 0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] receive server reply:%s\n&quot;, __func__, recvbuf);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//解析服务器的ip
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;vector&lt;std::string&gt; recvInfo;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;cstr_split(recvbuf, recvInfo);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(recvInfo.size() == 2 &amp;&amp; recvInfo == REPLAY_INFO)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;std::string serverIP = recvInfo;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;bHasGetServerIP = true;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;th_tcp_client = thread(tcp_client_thread, serverIP, localIP);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;th_tcp_client.join();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;else if (num == -1 &amp;&amp; errno == EAGAIN)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] receive timeout\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp;} &nbsp;
&nbsp;} &nbsp;

&nbsp; &nbsp;return 0; &nbsp;
}</pre>

<h3 cid="n47" mdtype="heading">3.2.2 客户端进行TCP连接</h3>

<p cid="n48" mdtype="paragraph"><strong>在获取到服务端的IP后,再开启一个线程,与服务端建立TCP连接,并进行数据通信</strong>,该线程的实现逻辑如下:</p>

<ul cid="n49" data-mark="-" mdtype="list">
        <li cid="n50" mdtype="list_item">
        <p cid="n51" mdtype="paragraph">创建一个socket,类型为TCP数据流(SOCK_STREAM)</p>
        </li>
        <li cid="n52" mdtype="list_item">
        <p cid="n53" mdtype="paragraph">sockaddrd的IP设置为刚才获取的服务端的IP(serverIP,例如192.168.1.101)</p>
        </li>
        <li cid="n54" mdtype="list_item">
        <p cid="n55" mdtype="paragraph">向服务端请求连接(connect)</p>
        </li>
        <li cid="n56" mdtype="list_item">
        <p cid="n57" mdtype="paragraph">连接成功之后,可以发送自定义的数据(send),这里发送的一串字母&quot;abcdefg&quot;加上自己的IP地址</p>
        </li>
        <li cid="n58" mdtype="list_item">
        <p cid="n59" mdtype="paragraph">如果服务端会还会回复信息,可以进行接收(recv),这里的接收设置为非阻塞模式(MSG_DONTWAIT),这样在服务端没有回复数据的情况下,客户端也不会一直等待,能够再次发送自己的数据</p>
        </li>
</ul>

<p cid="n60" mdtype="paragraph"><strong>具体的代码实现如下:</strong></p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
void tcp_client_thread(std::string serverIP, std::string localIP)
{
&nbsp; &nbsp;printf(&quot;[%s] in, prepare connect serverIP:%s\n&quot;, __func__, serverIP.c_str());
&nbsp; &nbsp;
    //创建客户端套接字文件
    int tcpClientSocket= socket(AF_INET, SOCK_STREAM, 0);
&nbsp; &nbsp;
    //初始化服务器端口地址
&nbsp; &nbsp;struct sockaddr_in servaddr;
    bzero(&amp;servaddr, sizeof(servaddr)) ;
    servaddr.sin_family= AF_INET;
    inet_pton(AF_INET, serverIP.c_str(), &amp;servaddr.sin_addr);
    servaddr.sin_port= htons(SERV_PORT);
&nbsp; &nbsp;
    //请求连接
    connect(tcpClientSocket, (struct sockaddr*)&amp;servaddr, sizeof (servaddr));
&nbsp; &nbsp;
&nbsp; &nbsp;//要向服务器发送的信息
&nbsp; &nbsp;char buf ;
    std::string msg = &quot;abcdefg&quot; + std::string(&quot;(&quot;) + localIP + std::string(&quot;)&quot;);
&nbsp; &nbsp;while(1)
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;//发送数据
&nbsp; &nbsp; &nbsp; &nbsp;send(tcpClientSocket, msg.c_str(), msg.length(),0);
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] send to server: %s\n&quot;, __func__, msg.c_str());
&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;//接收服务器返回的数据
&nbsp; &nbsp; &nbsp; &nbsp;int n= recv(tcpClientSocket, buf, MAXLINE, MSG_DONTWAIT); //非阻塞读取
&nbsp; &nbsp; &nbsp; &nbsp;if(n&gt;0)
&nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] Response from server: %s\n&quot;, __func__, buf);
&nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;sleep(2);
&nbsp;}
    //关闭连接
    close(tcpClientSocket) ;
}</pre>

<p cid="n62" mdtype="paragraph">&nbsp;</p>

<h2 cid="n63" mdtype="heading">3.3 服务端程序</h2>

<p cid="n64" mdtype="paragraph"><strong>服务端程序,主要设计了2个线程来分别实现对客户端UDP广播的处理和对客户端TCP连接的处理</strong>,两个功能独立开来,可以实现对多个客户端的UDP请求和TCP请求进行处理。</p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
int main() &nbsp;
{ &nbsp;
&nbsp; &nbsp;thread th1(recv_broadcast_thread);
&nbsp; &nbsp;thread th2(tcp_server_thread);
&nbsp; &nbsp;th1.join();
&nbsp; &nbsp;th2.join();

&nbsp; &nbsp;return 0; &nbsp;
}</pre>

<h3 cid="n66" mdtype="heading">3.3.1 服务端处理UDP广播</h3>

<p cid="n67" mdtype="paragraph"><strong>接收客户端广播信息的处理线程</strong>的主要逻辑为:</p>

<ul cid="n68" data-mark="-" mdtype="list">
        <li cid="n69" mdtype="list_item">
        <p cid="n70" mdtype="paragraph">获取自己的IP(用于回复给客户端,客户端获取到IP后进行TCP连接)</p>
        </li>
        <li cid="n71" mdtype="list_item">
        <p cid="n72" mdtype="paragraph">创建一个socket,类型为UDP数据报(SOCK_DGRAM)</p>
        </li>
        <li cid="n73" mdtype="list_item">
        <p cid="n74" mdtype="paragraph">sockaddrd的IP设置为接收所有IP(INADDR_ANY,0.0.0.0),并进行绑定(bind)</p>
        </li>
        <li cid="n75" mdtype="list_item">
        <p cid="n76" mdtype="paragraph">为socket添加广播属性(setsockopt,SO_BROADCAST)</p>
        </li>
        <li cid="n77" mdtype="list_item">
        <p cid="n78" mdtype="paragraph">接收UDP广播信息(recvfrom),这里是默认的阻塞接收,没有广播信息则一直等待</p>
        </li>
        <li cid="n79" mdtype="list_item">
        <p cid="n80" mdtype="paragraph">收到客户端的UDP广播信息后,解析信息,判断确实是要获取IP后,将自己的IP信息按照规定的格式发送出去</p>
        </li>
</ul>

<p cid="n81" mdtype="paragraph"><strong>具体的代码实现如下:</strong></p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
//接收客户端广播信息的处理线程, 收到客户端的UDP广播后, 将自己(服务端)的IP发送回去
void recv_broadcast_thread()
{
&nbsp; &nbsp;std::string localIP = &quot;&quot;;
&nbsp; &nbsp;if (true == get_local_ip(localIP))
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] localIP: [%s] %s\n&quot;, __func__, ETH_NAME, localIP.c_str());
&nbsp;}
&nbsp; &nbsp;else
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] get local ip err!\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp;return;
&nbsp;}

&nbsp; &nbsp;int sock = -1; &nbsp;
&nbsp; &nbsp;if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) &nbsp;
&nbsp;{ &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] socket error\n&quot;, __func__);
&nbsp; &nbsp; &nbsp; &nbsp;return; &nbsp;
&nbsp;} &nbsp; &nbsp;

&nbsp; &nbsp;struct sockaddr_in udpServerAddr; &nbsp;
&nbsp; &nbsp;bzero(&amp;udpServerAddr, sizeof(struct sockaddr_in)); &nbsp;
&nbsp; &nbsp;udpServerAddr.sin_family = AF_INET; &nbsp;
&nbsp; &nbsp;udpServerAddr.sin_addr.s_addr = htonl(INADDR_ANY); &nbsp;
&nbsp; &nbsp;udpServerAddr.sin_port = htons(6000); &nbsp;
&nbsp; &nbsp;int len = sizeof(sockaddr_in);
&nbsp; &nbsp;
&nbsp; &nbsp;if(bind(sock,(struct sockaddr *)&amp;(udpServerAddr), sizeof(struct sockaddr_in)) == -1) &nbsp;
&nbsp;{ &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] bind error\n&quot;, __func__); &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;return; &nbsp;
&nbsp;} &nbsp;
&nbsp; &nbsp;
&nbsp; &nbsp;set_sockopt_broadcast(sock);

&nbsp; &nbsp;char smsg = {0}; &nbsp;

&nbsp; &nbsp;while(1) &nbsp;
&nbsp;{ &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;//从广播地址接收消息 &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;int ret=recvfrom(sock, smsg, 100, 0, (struct sockaddr*)&amp;udpServerAddr, (socklen_t*)&amp;len); &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;if(ret&lt;=0) &nbsp;
&nbsp; &nbsp; &nbsp;{ &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] read error, ret:%d\n&quot;, __func__, ret); &nbsp;
&nbsp; &nbsp; &nbsp;} &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;else &nbsp;
&nbsp; &nbsp; &nbsp;{ &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s]receive: %s\n&quot;, __func__, smsg);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;vector&lt;std::string&gt; recvInfo;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;cstr_split(smsg, recvInfo);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//将自己的IP回应给请求的客户端
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(recvInfo.size() == 2 &amp;&amp; recvInfo == REQUEST_INFO)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;std::string clientIP = recvInfo;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;std::string replyInfo = REPLAY_INFO + INFO_SPLIT + localIP;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ret = sendto(sock, replyInfo.c_str(), replyInfo.length(), 0, (struct sockaddr *)&amp;udpServerAddr, len);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(ret&lt;0) &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{ &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] sendto error, ret: %d\n&quot;, __func__, ret); &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;} &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;else &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{ &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] reply ok, msg: %s\n&quot;, __func__, replyInfo.c_str()); &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;} &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp;} &nbsp;

&nbsp; &nbsp; &nbsp; &nbsp;sleep(1); &nbsp;
&nbsp;}
}</pre>

<h3 cid="n83" mdtype="heading">3.3.2 服务端处理客户端的TCP连接</h3>

<p cid="n84" mdtype="paragraph"><strong>TCP服务器线程, 用于接受客户端的连接</strong>, 主要逻辑如下:</p>

<ul cid="n85" data-mark="-" mdtype="list">
        <li cid="n86" mdtype="list_item">
        <p cid="n87" mdtype="paragraph">创建一个socket,命名为listenfd,类型为TCP数据流(SOCK_STREAM)</p>
        </li>
        <li cid="n88" mdtype="list_item">
        <p cid="n89" mdtype="paragraph">sockaddrd的IP设置为接收所有IP(INADDR_ANY,0.0.0.0),并进行绑定(bind)</p>
        </li>
        <li cid="n90" mdtype="list_item">
        <p cid="n91" mdtype="paragraph">监听,并设置最大连接数(listen)</p>
        </li>
        <li cid="n92" mdtype="list_item">
        <p cid="n93" mdtype="paragraph">创建一个epoll,来处理多客户端请求时(epoll_create)</p>
        </li>
        <li cid="n94" mdtype="list_item">
        <p cid="n95" mdtype="paragraph">将TCP socket添加到epoll进行监听(epoll_ctl,EPOLLIN)</p>
        </li>
        <li cid="n96" mdtype="list_item">
        <p cid="n97" mdtype="paragraph">epoll等待事件到来(epoll_wait)</p>
        </li>
        <li cid="n98" mdtype="list_item">
        <p cid="n99" mdtype="paragraph">epoll处理到来的事件</p>
        </li>
        <li cid="n100" mdtype="list_item">
        <p cid="n101" mdtype="paragraph">如果到来的是listenfd,说明有新的客户端请求连接,TCP服务端则接受请求(accept),然后将对应的客户端fd添加到epoll进行监听(epoll_ctl,EPOLLIN)</p>
        </li>
        <li cid="n102" mdtype="list_item">
        <p cid="n103" mdtype="paragraph">如果到来的不是listenfd,说明有已连接的客户端发来的数据信息,则读取信息(read)</p>
        </li>
</ul>

<p cid="n104" mdtype="paragraph"><strong>具体的代码实现如下:</strong></p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
//TCP服务器线程, 用于接受客户端的连接, 并接收客户端的信息
void tcp_server_thread()
{
    //创建服务器端套接字文件
    int listenfd=socket(AF_INET, SOCK_STREAM, 0);
&nbsp; &nbsp;
    //初始化服务器端口地址
&nbsp; &nbsp;struct sockaddr_in tcpServerAddr;
    bzero(&amp;tcpServerAddr, sizeof(tcpServerAddr));
    tcpServerAddr.sin_family=AF_INET;
    tcpServerAddr.sin_addr.s_addr= htonl(INADDR_ANY);
    tcpServerAddr.sin_port=htons(SERV_PORT);
&nbsp; &nbsp;
    //将套接字文件与服务器端口地址绑定
    bind(listenfd, (struct sockaddr *)&amp;tcpServerAddr, sizeof (tcpServerAddr)) ;
&nbsp; &nbsp;
    //监听,并设置最大连接数为20
    listen(listenfd, 20);
    printf(&quot;[%s] Accepting connections... \n&quot;, __func__);
&nbsp; &nbsp;
&nbsp; &nbsp;//通过epoll来监控多个客户端的请求
&nbsp; &nbsp;int epollfd;
&nbsp; &nbsp;struct epoll_event events;
&nbsp; &nbsp;int num;
&nbsp; &nbsp;char buf;
&nbsp; &nbsp;memset(buf,0,MAXSIZE);
&nbsp; &nbsp;epollfd = epoll_create(FDSIZE);
&nbsp; &nbsp;printf(&quot;[%s] create epollfd:%d\n&quot;, __func__, epollfd);

&nbsp; &nbsp;//添加监听描述符事件
&nbsp; &nbsp;epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, listenfd, EPOLLIN);
&nbsp; &nbsp;while(1)
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;//获取已经准备好的描述符事件
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] epollfd:%d epoll_wait...\n&quot;, __func__, epollfd);
&nbsp; &nbsp; &nbsp; &nbsp;num = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
&nbsp; &nbsp; &nbsp; &nbsp;for (int i = 0;i &lt; num;i++)
&nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int fd = events.data.fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//listenfd说明有新的客户端请求连接
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if ((fd == listenfd) &amp;&amp;(events.events &amp; EPOLLIN))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//accept客户端的请求
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;struct sockaddr_in cliaddr;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;socklen_t &nbsp;cliaddrlen = sizeof(cliaddr);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int clifd = accept(listenfd,(struct sockaddr*)&amp;cliaddr,&amp;cliaddrlen);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (clifd == -1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;perror(&quot;accpet error:&quot;);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] accept a new client(fd:%d): %s:%d\n&quot;,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; __func__, clifd, inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//将客户端fd添加到epoll进行监听
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;epoll_set_fd_a_event(epollfd, EPOLL_CTL_ADD, clifd, EPOLLIN);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//收到已连接的客户端fd的消息
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;else if (events.events &amp; EPOLLIN)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;memset(buf,0,MAXSIZE);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//读取客户端的消息
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int nread = read(fd,buf,MAXSIZE);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (nread == -1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;perror(&quot;read error:&quot;);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;close(fd);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;epoll_set_fd_a_event(epollfd, EPOLL_CTL_DEL, fd, EPOLLIN);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;else if (nread == 0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] client(fd:%d) close.\n&quot;, __func__, fd);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;close(fd);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;epoll_set_fd_a_event(epollfd, EPOLL_CTL_DEL, fd, EPOLLIN);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//将客户端的消息打印处理, 并表明是哪里客户端fd发来的消息
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] read message from fd:%d ---&gt; %s\n&quot;, __func__, fd, buf);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp;}
&nbsp;}

&nbsp; &nbsp;close(epollfd);
}</pre>

<p cid="n106" mdtype="paragraph">为epoll中的某个fd添加、修改或删除某个事件,这里封装成了一个函数:</p>

<pre style="background:#555; padding:10px; color:#ddd !important;">
//为epoll中的某个fd添加/修改/删除某个事件
bool epoll_set_fd_a_event(int epollfd, int op, int fd, int event)
{
&nbsp; &nbsp;if (EPOLL_CTL_ADD == op || EPOLL_CTL_MOD == op || EPOLL_CTL_DEL == op)
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;struct epoll_event ev;
&nbsp; &nbsp; &nbsp; &nbsp;ev.events = event;
&nbsp; &nbsp; &nbsp; &nbsp;ev.data.fd = fd;
&nbsp; &nbsp; &nbsp; &nbsp;epoll_ctl(epollfd, op, fd, &amp;ev);
&nbsp; &nbsp; &nbsp; &nbsp;return true;
&nbsp;}
&nbsp; &nbsp;else
&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;printf(&quot;[%s] err op:%d\n&quot;, __func__, op);
&nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp;}
}</pre>

<p cid="n108" mdtype="paragraph">&nbsp;</p>

<h1 cid="n109" mdtype="heading">4 测试结果</h1>

<p cid="n110" mdtype="paragraph">这里测试了4种不同的情况,来验证客户端可以自动获取到服务端的IP,并进行TCP连接,另外,服务端也可以处理多个客户端的请求:</p>

<ul cid="n111" data-mark="-" mdtype="list">
        <li cid="n112" mdtype="list_item">
        <p cid="n113" mdtype="paragraph">1)<strong>单个客户端连接服务端</strong></p>
        </li>
</ul>

<p cid="n114" mdtype="paragraph"></p>

<p cid="n115" mdtype="paragraph">&nbsp;</p>

<ul cid="n116" data-mark="-" mdtype="list">
        <li cid="n117" mdtype="list_item">
        <p cid="n118" mdtype="paragraph">2)<strong>单个客户端连接并中止后,另一个客户端再次连接服务端</strong></p>
        </li>
</ul>

<p cid="n119" mdtype="paragraph"></p>

<p cid="n120" mdtype="paragraph">&nbsp;</p>

<ul cid="n121" data-mark="-" mdtype="list">
        <li cid="n122" mdtype="list_item">
        <p cid="n123" mdtype="paragraph">3)<strong>客户端先启动后,服务端再启动,客户端依然能在服务端启动后连接到服务端</strong></p>
        </li>
</ul>

<p cid="n124" mdtype="paragraph"></p>

<p cid="n125" mdtype="paragraph">&nbsp;</p>

<ul cid="n126" data-mark="-" mdtype="list">
        <li cid="n127" mdtype="list_item">
        <p cid="n128" mdtype="paragraph">4)<strong>两个客户端现后进行连接服务端</strong></p>
        </li>
</ul>

<p cid="n129" mdtype="paragraph"></p>

<h1 cid="n130" mdtype="heading">5 总结</h1>

<p cid="n131" mdtype="paragraph">本篇介绍了在TCP通信中,客户端通过UDP广播,实现自动获取服务端的IP地址,并进行TCP连接的具体方法,并通过代码实现,来测试此方案是实际效果,为了使服务端能够处理多个客户端的请求,这里使用了多线程编程,以及epoll机制来实现多客户端的处理。</p>
</div><script>                                        var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;"   style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
                                       
                                        if(parseInt(discuz_uid)==0){
                                                                                                (function($){
                                                        var postHeight = getTextHeight(400);
                                                        $(".showpostmsg").html($(".showpostmsg").html());
                                                        $(".showpostmsg").after(loginstr);
                                                        $(".showpostmsg").css({height:postHeight,overflow:"hidden"});
                                                })(jQuery);
                                        }                </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script>

lugl4313820 发表于 2022-10-7 14:07

<p>本篇介绍了在TCP通信中,客户端通过UDP广播,实现自动获取服务端的IP地址,并进行TCP连接的具体方法,并通过代码实现,来测试此方案是实际效果,为了使服务端能够处理多个客户端的请求,这里使用了多线程编程,以及epoll机制来实现多客户端的处理。</p>

<p>内容非常好,慢慢学习,谢谢分享!</p>

sgsg 发表于 2024-9-12 22:46

<p>这篇文章来的真及时啊,正好最近在研究TCP协议栈</p>

<p>&nbsp;</p>
页: [1]
查看完整版本: Linux网络编程-TCP客户端如何获取要连接的服务端IP?