跳转到内容

C 编程/UNIX 中的网络

来自维基教科书,开放的书籍,开放的世界
上一个:复杂类型 C 编程 下一个:常见做法

在 UNIX 下的网络编程在 C 中相对简单。

本指南假设您已经对 C、UNIX 和网络有了一般性的了解。


一个简单的客户端

[编辑 | 编辑源代码]

首先,我们将看一下您可以做的最简单的事情之一:初始化一个流连接并从远程服务器接收一条消息。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
 
#define MAXRCVLEN 500
#define PORTNUM 2300
 
int main(int argc, char *argv[])
{
   char buffer[MAXRCVLEN + 1]; /* +1 so we can add null terminator */
   int len, mysocket;
   struct sockaddr_in dest; 
 
   mysocket = socket(AF_INET, SOCK_STREAM, 0);
  
   memset(&dest, 0, sizeof(dest));                /* zero the struct */
   dest.sin_family = AF_INET;
   dest.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* set destination IP number - localhost, 127.0.0.1*/ 
   dest.sin_port = htons(PORTNUM);                /* set destination port number */
 
   connect(mysocket, (struct sockaddr *)&dest, sizeof(struct sockaddr_in));
  
   len = recv(mysocket, buffer, MAXRCVLEN, 0);
 
   /* We have to null terminate the received data ourselves */
   buffer[len] = '\0';
 
   printf("Received %s (%d bytes).\n", buffer, len);
 
   close(mysocket);
   return EXIT_SUCCESS;
}

这是最基本的客户端;在实践中,我们会检查我们调用的每个函数是否失败,但是为了清楚起见,错误检查被省略了。

如您所见,代码主要围绕 dest 展开,它是一个类型为 sockaddr_in 的结构体。此结构体存储有关我们要连接的机器的信息。

mysocket = socket(AF_INET, SOCK_STREAM, 0);

socket() 函数告诉我们的操作系统,我们想要一个用于套接字的文件描述符,我们可以用它来进行网络流连接;参数的含义现在大多无关紧要。

memset(&dest, 0, sizeof(dest));                /* zero the struct */
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = inet_addr("127.0.0.1"); /* set destination IP number */ 
dest.sin_port = htons(PORTNUM);                /* set destination port number */

现在我们开始进入有趣的部分

第一行使用 memset() 将结构体清零。

第二行设置地址族。这应该是与作为 socket() 的第一个参数传递的值相同的值;对于大多数目的,AF_INET 将起作用。

第三行是我们设置要连接的机器的 IP 的地方。变量 dest.sin_addr.s_addr 只是一个以大端格式存储的整数,但我们不必知道这一点,因为 inet_addr() 函数会为我们执行从字符串到大端整数的转换。

第四行设置目标端口号。htons() 函数将端口号转换为大端短整数。如果您的程序将仅在使用大端数字作为默认值的机器上运行,那么 dest.sin_port = 21 也可以使用。但是,为了可移植性,应该始终使用 htons()

现在所有准备工作都完成了,我们可以实际建立连接并使用它了

connect(mysocket, (struct sockaddr *)&dest, sizeof(struct sockaddr_in));

这告诉我们的操作系统使用套接字 mysocket 建立与 dest 中指定的机器的连接。

len = recv(mysocket, buffer, MAXRCVLEN, 0);

现在这从连接中接收最多 MAXRCVLEN 字节的数据,并将它们存储在缓冲区字符串中。接收到的字符数量由 recv() 返回。需要注意的是,接收到的数据不会在存储在缓冲区中时自动以空值结尾,因此我们需要使用 buffer[len] = '\0' 手动进行。

就是这样了!

在学习如何接收数据之后的下一步是学习如何发送数据。如果您理解了上一节的内容,那么这非常容易。您只需使用 send() 函数,该函数使用与 recv() 相同的参数。如果在我们之前的示例中,buffer 包含我们要发送的文本,其长度存储在 len 中,我们就会写 send(mysocket, buffer, len, 0)send() 返回发送的字节数。请记住,send() 可能由于各种原因无法发送所有字节,因此检查其返回值是否等于您尝试发送的字节数非常重要。在大多数情况下,可以通过重新发送未发送的数据来解决此问题。

一个简单的服务器

[编辑 | 编辑源代码]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
 
#define PORTNUM 2300
 
int main(int argc, char *argv[])
{
    char* msg = "Hello World !\n";
  
    struct sockaddr_in dest; /* socket info about the machine connecting to us */
    struct sockaddr_in serv; /* socket info about our server */
    int mysocket;            /* socket used to listen for incoming connections */
    socklen_t socksize = sizeof(struct sockaddr_in);

    memset(&serv, 0, sizeof(serv));           /* zero the struct before filling the fields */
    serv.sin_family = AF_INET;                /* set the type of connection to TCP/IP */
    serv.sin_addr.s_addr = htonl(INADDR_ANY); /* set our address to any interface */
    serv.sin_port = htons(PORTNUM);           /* set the server port number */    

    mysocket = socket(AF_INET, SOCK_STREAM, 0);
  
    /* bind serv information to mysocket */
    bind(mysocket, (struct sockaddr *)&serv, sizeof(struct sockaddr));

    /* start listening, allowing a queue of up to 1 pending connection */
    listen(mysocket, 1);
    int consocket = accept(mysocket, (struct sockaddr *)&dest, &socksize);
  
    while(consocket)
    {
        printf("Incoming connection from %s - sending welcome\n", inet_ntoa(dest.sin_addr));
        send(consocket, msg, strlen(msg), 0); 
        close(consocket);
        consocket = accept(mysocket, (struct sockaddr *)&dest, &socksize);
    }

    close(mysocket);
    return EXIT_SUCCESS;
}

表面上看,这与客户端非常相似。第一个重要的区别是,我们不是使用有关我们要连接的机器的信息来创建 sockaddr_in,而是使用有关服务器的信息来创建它,然后我们将它 bind() 到套接字。这使机器知道在 sockaddr_in 中指定的端口上接收到的数据应该由我们指定的套接字处理。

然后,listen() 函数告诉我们的程序使用给定的套接字开始监听。listen() 的第二个参数允许我们指定可以排队的连接的最大数量。每次与服务器建立连接时,它都会被添加到队列中。我们使用 accept() 函数从队列中获取连接。如果没有连接在队列中等待,则程序会一直等待直到收到连接。accept() 函数返回另一个套接字。此套接字本质上是一个“会话”套接字,并且可以专门用于与我们从队列中取出的连接进行通信。原始套接字 (mysocket) 继续在指定端口上监听以获取更多连接。

获得“会话”套接字后,我们可以像客户端一样处理它,使用 send()recv() 来处理数据传输。

请注意,此服务器一次只能接受一个连接;如果您想同时处理多个客户端,则需要 fork() 掉独立的进程,或者使用线程来处理连接。

有用的网络函数

[编辑 | 编辑源代码]
int gethostname(char *hostname, size_t size);

参数是字符数组的指针和该数组的大小。如果可能,它会找到主机名并将其存储在数组中。失败时返回 -1。

struct hostent *gethostbyname(const char *name);

此函数获取有关域名信息并将其存储在 hostent 结构体中。hostent 结构体中最有用的部分是 (char**) h_addr_list 字段,它是一个以空值结尾的与该域名关联的 IP 地址数组。字段 h_addr 是指向 h_addr_list 数组中第一个 IP 地址的指针。失败时返回 NULL

常见问题解答

[编辑 | 编辑源代码]

无状态连接怎么样?

[编辑 | 编辑源代码]

如果您不想在您的程序中利用 TCP 的特性,而想使用 UDP 连接,那么您只需在 socket() 的调用中将 SOCK_STREAM 替换为 SOCK_DGRAM,并以相同的方式使用结果。请记住,UDP 不保证数据包的传递和传递顺序,因此检查非常重要。

如果您想利用 UDP 的特性,那么您可以使用 sendto()recvfrom(),它们的功能与 send()recv() 类似,只是您需要提供额外的参数来指定您与谁进行通信。

如何检查错误?

[编辑 | 编辑源代码]

函数 socket()recv()connect() 都会在失败时返回 -1,并使用 errno 来获取更多详细信息。

上一个:复杂类型 C 编程 下一个:常见做法
华夏公益教科书