Printf 使程序在 C 中工作,htonl 和 ntohl 不工作?

Printf makes program work in C, htonl and ntohl not working?

这是一个Linux系统,C语言,涉及到网络编程。用于文件传输程序。

我一直遇到这个问题,这段代码无法正常工作。它要么完全成功,要么客户端中的 while 循环永远不会结束。我发现这是因为 fileLength 变量有时会是一个巨大的(负或正)值,我认为这是由于使用 ntohl 时犯了一些错误。当我输入 print 语句时,它似乎工作得很好,没有错误。

客户端代码如下:

        //...here includes relevant header files

        int main (int argc, char *argv[]) {
            //socket file descriptor
            int sockfd;

            if (argc != 2) {
                fprintf (stderr, "usage: client hostname\n");
                exit(1);
            }

            //...creates socket file descriptor, connects to server


            //create buffer for filename
            char name[256];
            //recieve filename into name buffer, bytes recieved stored in numbytes
            if((numbytes = recv (sockfd, name, 255 * sizeof (char), 0)) == -1) {
                perror ("recv");
                exit(1);
            }
            //Null terminator after the filename
            name[numbytes] = '[=10=]';
            //length of the file to recieve from server
            long fl;
            memset(&fl, 0, sizeof fl);
            //recieve filelength from server
            if((numbytes = recv (sockfd, &fl, sizeof(long), 0)) == -1) {
                perror ("recv");
                exit(1);
            }

            //convert filelength to host format
            long fileLength = ntohl(fl);


            //check to make sure file does not exist, so that the application will not overwrite exisitng files
            if (fopen (name, "r") != NULL) {
                fprintf (stderr, "file already present in client directory\n");
                exit(1);
            }
            //open file called name in write mode
            FILE *filefd = fopen (name, "wb");
            //variable stating amount of data recieved
            long bytesTransferred = 0;
            //Until the file is recieved, keep recieving
            while (bytesTransferred < fileLength) {
                printf("transferred: %d\ntotal: %d\n", bytesTransferred, fileLength);
                //set counter at beginning of unwritten segment
                fseek(filefd, bytesTransferred, SEEK_SET);
                //buffer of 256 bytes; 1 byte for byte-length of segment, 255 bytes of data
                char buf[256];
                //recieve segment from server
                if ((numbytes = recv (sockfd, buf, sizeof buf, 0)) == -1) {
                    perror ("recv");
                    exit(1);
                }

                //first byte of buffer, stating number of bytes of data in recieved segment
                //converting from char to short requires adding 128, since the char ranges from -128 to 127
                short bufLength = buf[0] + 128;

                //write buffer into file, starting after the first byte of the buffer
                fwrite (buf + 1, 1, bufLength * sizeof (char), filefd);
                //add number of bytes of data recieved to bytesTransferred
                bytesTransferred += bufLength;

            }
            fclose (filefd);
            close (sockfd);

            return 0;
        }

这是服务器代码:

        //...here includes relevant header files

        int main (int argc, char *argv[]) {
            if (argc != 2) {
                fprintf (stderr, "usage: server filename\n");
                exit(1);
            }
            //socket file descriptor, file descriptor for specific client connections
            int sockfd, new_fd;


            //...get socket file descriptor for sockfd, bind sockfd to predetermined port, listen for incoming connections



            //...reaps zombie processes


            printf("awaiting connections...\n");

            while(1) {
                //...accepts any incoming connections, gets file descriptor and assigns to new_fd

                if (!fork()) {
                    //close socket file discriptor, only need file descriptor for specific client connection
                    close (sockfd);
                    //open a file for reading
                    FILE *filefd = fopen (argv[1], "rb");
                    //send filename to client
                    if (send (new_fd, argv[1], strlen (argv[1]) * sizeof(char), 0) == -1)
                    { perror ("send"); }
                    //put counter at end of selected file, and  find length
                    fseek (filefd, 0, SEEK_END);
                    long fileLength = ftell (filefd);
                    //convert length to network form and send it to client

                    long fl = htonl(fileLength);
                    //Are we sure this is sending all the bytes??? TEST
                    if (send (new_fd, &fl, sizeof fl, 0) == -1)
                    { perror ("send"); }
                    //variable stating amount of data unsent
                    long len = fileLength;
                    //Until file is sent, keep sending
                    while(len > 0) {
                        printf("remaining: %d\ntotal: %d\n", len, fileLength);
                        //set counter at beginning of unread segment
                        fseek (filefd, fileLength - len, SEEK_SET);
                        //length of the segment; 255 unless last segment
                        short bufLength;
                        if (len > 255) {
                            len -= 255;
                            bufLength = 255;
                        } else {
                            bufLength = len;
                            len = 0;
                        }
                        //buffer of 256 bytes; 1 byte for byte-length of segment, 255 bytes of data
                        char buf[256];
                        //Set first byte of buffer as the length of the segment
                        //converting short to char requires subtracting 128
                        buf[0] = bufLength - 128;
                        //read file into the buffer starting after the first byte of the buffer
                        fread(buf + 1, 1, bufLength * sizeof(char), filefd);
                        //Send data too client
                        if (send (new_fd, buf, sizeof buf, 0) == -1)
                        { perror ("send"); }
                    }
                    fclose (filefd);
                    close (new_fd);
                    exit (0);
                }
                close (new_fd);
            }

            return 0;
        }

注意:我已经稍微简化了代码,希望它更清晰。 以 //... 开头的任何内容都代表一堆代码

即使您从该代码中删除了它,我也会做出有根据的猜测,并假设您在这里使用的是 TCP 或其他一些流协议。这意味着服务器发送的数据是字节流,recv 调用将不会对应于它们与 send 调用获得的数据量。

您的第一个 recv 调用只获取一个字节的数据同样合法,因为它是获取文件名、文件大小和文件的一半。

你说

When I put in a print statement,

但你没有说在哪里。我将在这里进行另一个有根据的猜测,并猜测您是在发送文件长度之前在服务器上完成的。这恰好足以撼动事情,以至于在连接上发送的数据量恰好恰好与您在客户端上的预期相匹配。

您需要定义一个协议。也许从文件名的长度开始,然后是文件名,然后是文件的长度。或者总是发送 256 字节的文件名,不管它有多长。或者将文件名作为以 0 结尾的字符串发送,并尝试从中找出数据。但是你永远不能假设仅仅因为你用 X 字节调用了 send,那么 recv 调用将得到 X 字节。

您似乎假设每个 send() 要么传输指定的全部字节数,要么会出错,并且每个都将与另一侧的 recv() 完美配对,这样 recv() 就可以准确接收到 send() 发送的字节数(或错误输出),不多也不少。这些都不是安全的假设。

您没有显示用于设置网络连接的代码。如果您使用的是基于数据报的协议(即 UDP),那么您更有可能获得预期的 send/receive 边界匹配,但您需要考虑数据包丢失或损坏的可能性。如果您使用的是基于流的协议(即 TCP),那么您不必太担心数据丢失或损坏,但您根本没有理由期望边界匹配行为。

你至少需要三样东西:

  • 网络层之上的应用层协议。您已经掌握了其中的一部分,例如您如何首先传输文件长度以告知客户预期的内容,但您需要对所有传输的非预定、固定长度的数据执行类似的操作。或者,发明另一种方式来传达数据边界。

  • 每个旨在传输一个以上字节的 send() / write() 都必须在循环中执行,以适应被分成多个部分的传输。 return 值告诉您有多少请求的字节已传输(或至少有多少已移交到网络堆栈),如果少于请求的字节数,您必须循环返回以尝试传输其余字节。

  • 每个旨在传输一个以上字节的 recv() / read() 都必须在一个循环中执行,以适应被分成多个部分的传输。我建议按照 send() 中描述的相同行构建它,但您也可以选择接收数据,直到看到预先安排的分隔符。然而,基于定界符的方法更为复杂,因为它需要在接收端进行额外的缓冲。

如果没有这些措施,您的服务器和客户端很容易失去同步。其中可能的结果是客户端将部分文件名或部分文件内容解释为文件长度。

嗯,经过一些测试,我发现导致问题的原因确实与htonl()有关,尽管一开始我仍然读取数据不正确。这并不是说 htonl() 根本不起作用,而是我没有意识到 'long' 的长度因系统架构而异(感谢@tofro)。也就是说一个'long'整数在32位和64位操作系统上的长度分别是4字节和8字节。以及用于 4 字节整数的 htonl() 函数(来自 arpa/inet.h)。我使用的是 64 位 OS,这解释了为什么该值被捏造了。我通过使用 int32_t 变量(来自 stdint.h)来存储文件长度来解决这个问题。所以在这种情况下的主要问题不是它变得不同步(我认为)。但至于大家对开发实际协议的建议,我想我明白你的意思,我完全理解为什么它很重要,我目前正在朝着这个方向努力。谢谢大家的帮助。

编辑:好吧,现在已经好几年了,我知道的多了一点,我知道这个解释没有意义。 long 比我预期的要大(8 个字节而不是 4 个字节)会导致的所有结果是正在进行一些隐式转换。我在原始代码中使用了 sizeof(long),而不是将其硬编码为 4 个字节,因此我的特定(错误)假设不应该产生我看到的错误。

问题几乎可以肯定是其他人所说的:对 recv 的一次调用没有获得代表文件长度的所有字节。当时我怀疑这是我看到的行为的真正原因,因为我发送的文件名(任意长度)从未被部分发送(即客户端总是创建一个正确文件名的文件)。只有文件长度搞砸了。我当时的假设是 recv 主要遵守消息边界,虽然 recv 可能 只发送部分数据,但更有可能是全部发送,我的代码中还有另一个错误。我现在知道这根本不是真的,而且 .

我有点好奇为什么我也没有看到其他意外行为(例如接收端的文件名错误),我想进一步调查,但尽管设法找到了文件,我现在似乎无法重现该问题。我想我永远不会知道,但至少我明白这里的主要问题。

我认为这个问题实际上是你和其他人所说的一切的复合体。在服务器代码中,您像这样发送文件名:

send (new_fd, argv[1], strlen (argv[1]) * sizeof(char), 0);

并像这样在客户端接收它:

recv (sockfd, name, 255 * sizeof (char), 0);

当文件名长度小于 255 时,这将导致问题。由于 TCP 是一种流协议(如@Art 所述),send 和 [= 之间没有真正的界限14=]s,这可能会导致您在意想不到的奇怪位置接收数据。

我的建议是先发送文件名的长度,例如:

// server
long namelen = htonl(strlen(argv[1]));
send (new_fd, &namelen, 4, 0);
send (new_fd, argv[1], strlen (argv[1]) * sizeof(char), 0);
// client
long namelen;
recv (sockfd, &namelen, 4, 0);
namelen = ntohl(namelen);
recv (sockfd, name, namelen * sizeof (char), 0);

这将确保您始终知道文件名的确切长度,并确保您不会不小心从文件中间的某处读取文件长度(这是我预计当前正在发生的情况) ).

编辑。

另外,发送大号号码时要小心。如果您对它们使用 sizeof 调用,您可能会发送和接收不同的大小。这就是为什么我在 sendrecv 中硬编码名称长度的原因,这样两边都不会混淆。