C 编程/UNIX 中的网络
在 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 来获取更多详细信息。