TCP 客户端/服务器:何时停止读取套接字

TCP client / server : when stop read socket

我在创建小型 ftp 客户端/服务器 (tcp)

时面临多个问题

客户端有提示。

如何停止接收问题。通过我的套接字从客户端向服务器发送数据,反之亦然。

例如,从服务器向客户端发送一些字符串。客户端如何知道何时停止读取套接字并离开 recv() 循环以打印回提示

这就是为什么我创建了 transfert functions 来知道何时通过预发送停止(如果它是最后一次发送)(CONT - DONE)并且它工作得很好。 (下面的代码)

然后我需要在服务器上执行类似 ls 的命令并将结果发送到客户端,所以我想到了 2 个选项。

第二个选项更清晰,但它让我面临第一个关于如何停止 recv() 循环的问题。

奖励问题:如何将 execv() 复制到字符串。由于 fd 参数,我想使用 mmap,但我仍然需要提前知道大小。

# define BUFFSIZE 512
# define DONE "DONE"
# define CONT "CONT"

int     send_to_socket(int sock, char *msg)
{
    size_t  len;
    int     ret[2];
    char    buff[BUFFSIZE+1];

    len = strlen(msg);
    bzero(buff, BUFFSIZE+1);
    strncpy(buff, msg, (len <= BUFFSIZE) ? len : BUFFSIZE);
    /*strncpy(buff, msg, BUFFSIZE);*/
    ret[0] = send(sock, (len <= BUFFSIZE) ? DONE : CONT, 4, 0);
    ret[1] = send(sock, buff, BUFFSIZE, 0);
    if (ret[0] <= 0 || ret[1] <= 0)
    {
        perror("send_to_socket");
        return (-1);
    }
    // recursive call
    if (len > BUFFSIZE)
        return (send_to_socket(sock, msg + BUFFSIZE));
    return (1);
}

char    *recv_from_socket(int cs)
{
    char    state[5];
    char    buff[BUFFSIZE+1];
    char    *msg;
    int     ret[2];

    msg = NULL;
    while (42)
    {
        bzero(state, 5);
        bzero(buff, BUFFSIZE+1);
        ret[0] = recv(cs, state, 4, 0);
        ret[1] = recv(cs, buff, BUFFSIZE, 0);
        if (ret[0] <= 0 || ret[1] <= 0)
        {
            perror("recv_from_socket");
            return (NULL);
        }
        // strfljoin(); concat the strings and free the left parameter
        msg = (msg) ? strfljoin(msg, buff) : strdup(buff);
        if (strnequ(state, DONE, 4))
            break ;
    }
    return (msg);
}

有什么意义 而(42)

这与 而(1) 如果你只是希望它永远循环

如果我理解正确,你想使用 execv 来执行 'ls' 并捕获输出以便你可以发送它。

看看 'popen' 函数,它将创建一个管道文件描述符(和 return 它),然后执行命令,输出连接到管道。

然后您可以使用常规的 read() 系统调用来读取文件描述符的输出。

像这样

#include <stdio.h>
FILE *f
char buf[BUF_MAX]
f = popen("ls", "r")

if (!f) 
    // send error message or whatever
    return

while (fgets(buf, BUF_MAX, f)) 
    // send buf with your other socket function

pclose(f);

如果您不想使用 libc stdio,而只想使用低级系统例程,那么要做的就是检查 fork() 、 pipe() 和 dup() 系统调用,

首先使用 pipe()(给你两个文件描述符,每一端一个)

然后 fork()

然后关闭未使用的文件描述符(),

然后使用 dup() 或 dup2() 仅将新进程中的 fd 1 更改为之前管道调用的输出 fd 编号。

然后最后你 execv() 到 运行 "ls" 命令,它的输出将进入管道

并且在原始过程中,您从第一个 fd 中读取 () return 由早先的 pipe() 调用编辑

既然你想让客户端知道服务器何时结束,你可以这样做:

const int MSG_DATA = 1;
const int MSG_DONE = 2;
strict message {
  int messagetype;
  int len;
}
char buf[BUF_SIZE]


message msg;
// Put data into buf
// set msg.len
msg.messagetype= MSG_DATA;
send(msg,size of(msg))
send (buf,msg.len)

然后当你完成后。

msg.messagetype=MSG_DONE
msg.len = 0
send(msg,sizeof(msg))

在客户端:

Message msg;
Char buf[]

while()
   ...
   recv(&msg)
   ...
   if(msg.messagetype == MSG_DATA)
      recv(buf)
   elif msg.message type == MSG_DONE)
      DoSomething()

但是,请记住,您的客户端无论如何都应该始终接收数据包。

如果不是为了学校练习而不能随心所欲,最好使用现有的库,例如消息队列库,也许是 zeromq,而不是用困难的方式来做。

还有一个简单的curl C库,你可以用它来发送HTTP,它已经整理好了所有这些东西,也可以做不定长的内容。

此外(我假设您使用的是 TCP 套接字,而不是 UDP),连接的两端都不能发送 N 个字节,并且期望其他站点上的 recv 将获得 N 个字节。 recv 只会获取操作系统网络堆栈当前在其 TCP 缓冲区中接收和缓冲的内容。而且receiver也可以在一次recv中得到几个已经发送的chunk。网络上的数据包大小可能在 1400-1500 字节左右,您的服务器可能会发送一条几 kB 的消息,该消息将被分成几个数据包,并且可能在第一个数据包之后由接收方处理,在其余部分进来之前,或者您的服务器可能会用您的 DONE 或 CONT header 发送几条小消息,接收方可能会在一次 recv() 中获得所有这些信息。即使您很幸运并且 DONE 或 CONT 实际上位于 TCP 缓冲区的开头,您的代码也会读取 OS TCP 缓冲区中的所有内容,并且只会检查前 4 个字节以查看 DONE 或CONT 从第一条消息开始,并将所有其余消息视为数据。所以你真的需要发送一个长度字段。您可以完全废弃 DONE 或 CONT 并改为发送长度,并发送 0 长度来表示 DONE。然后你的客户端在收到时,可以 recv() 它可以得到的所有东西,进入你的缓冲区,然后使用长度字段依次处理该缓冲区中包含的每条消息。

您的判断是正确的,要通过 stream-oriented 套接字进行除无差别流之外的任何通信,您需要在通信双方之间应用某种 application-layer 协议。这就是你的 send_to_socket()recv_from_socket() 函数正在做的事情,尽管它们有缺陷。*

假设您确实需要使用 application-layer 协议,那么让子进程直接写入套接字根本不是一个选项,除非您的特定 ALP 可以容纳将整个程序输出封装为单个块,您正在使用的块无法做到。

话虽如此,您至少还有一个未考虑的其他选择:让父级将子级的输出发送到套接字 当子级产生输出时,而不是收集所有这些,然后才发送。这将涉及在子项和父项之间建立一个管道,并且可能是 send_to_socket() 的一个单独版本,它读取要从 FD 而不是从字符串发送的数据。您可能会一次累积一个 modest-sized 缓冲区。这种方法将是我的建议。

bonus question: how to dup execv() to a string. i thought using mmap thanks to the fd parameter, but i still need to know the size in advance.

mmap() 采用指定要映射的文件的文件描述符参数,但这并不意味着它只适用于任何文件描述符。它只能保证与指定常规文件和 shared-memory 对象的 FD 一起使用,您不能期望它适用于指定瞬态数据管道的 FD。要在内存中捕获子进程的输出,您需要像我为第三个选项描述的那样操作,但将读取的数据存储在 dynamically-allocated(并根据需要重新分配)缓冲区中,而不是将其发送到客户端,因为它被读取。这可能既昂贵又混乱。


* 你的功能有缺陷:

  1. 他们假设可以依赖 send()recv() 函数来准确传输请求的字节数,否则就会失败。

事实上,send()recv()都可能执行部分传输。为避免丢失数据和/或不同步,您 必须 将这些函数的 return 值与您尝试传输的字节数进行比较以检测部分传输,如果发生部分传输,您必须发出另一个调用以发送数据的余额。由于同样的事情可能会再次发生,因此您通常需要将整个事情放在一个循环中,不断调用 send()recv() 直到发送完所有数据或 善意的 发生故障。

  1. 递归对于发送函数来说是一个糟糕的实现选择。如果您有大量数据要发送,那么您可能会耗尽堆栈,而且每个递归函数调用的开销要比循环返回的开销大得多。

  2. 发送 fixed-length 块中的数据是不必要的,并且需要在发送之前将数据复制到单独的缓冲区的开销。

考虑发送消息长度而不是 "CONT" 或 "DONE",后跟那么多字节(但见上文)。您还可以将标志位合并到消息长度中以传达附加信息——例如,一个标志当前块是否是最后一个的位。

  1. 您的 send()recv() 调用可能因与连接及其持续可行性无关的原因而失败。例如,它们可以被信号中断。由于如果发送方和接收方之间的通信被中断,您将无法 re-sync 通信,因此您应该确保在发生错误时终止通信,尽管实际上不必由您的 send 和 recv 处理自己发挥作用。