网络编程

一、IP地址

一个IP地址就是一个32位无符号整数。网络程序将IP地址存放在如下所示的IP地址结构中:

1 struct in_addr
2 {
3     uint32_t s_addr;
4 }

在IP地址结构中存放的地址总是以(大端法)网络字节顺序存放的,主机字节顺序有大端法和小端法两种。

PS:术语“小端”和“大端”表示多字节值的哪一端(小端或大端)存储在该值的起始地址。

Unix提供了下面这样的函数在网络和主机字节顺序间实现转换:

1 #include <arpa/inet.h>
2 
3 // 返回网络字节顺序的值
4 uint32_t htonl(uint32_t hostlong);
5 uint16_t htons(uint16_t hostshort);
6 
7 // 返回主机字节顺序的值
8 uint32_t ntohl(uint32_t netlong);
9 uint16_t ntohs(uint16_t netshort);

应用程序使用inet_pton和inet_ntop函数来实现IP地址和点分十进制串之间的转换:

1 #include <arpa/inet.h>
2 
3 // 将一个点分十进制串(src)转换为一个二进制的网络字节顺序的IP地址。
4 // 若成功则为1,若src为非法点分十进制地址则为0,若出错则为-1。
5 int inet_pton(AF_INET, const char *src, void *dst);
6 
7 // 将一个二进制的网络字节顺序的IP地址(src)转换为它所对于的点分十进制表示。
8 // 若成功则指向点分十进制字符串的指针,若出错则为NULL。
9 const char* inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);

 

二、因特网域名

每台因特网主机都有本地定义的域名localhost,这个域名总是映射为回送地址127.0.0.1:

linux> nslookup localhost

Address: 127.0.0.1

在最简单的情况中,一个域名与一个IP地址之间是一一映射:

linux> nslookup whaleshark.ics.cs.cmu.edu

Address: 128.2.210.175

在某些情况下,多个域名可以映射为同一个IP地址:

linux> nslookup cs.mit.edu

Address: 18.62.1.6

linux> nslookup eecs.mit.edu

Address:18.62.1.6

在最通常的情况下,多个域名可以映射到同一组的多个IP地址:

linux> nslookup www.twitter.com

Address: 199.16.156.6

Address: 199.16.156.70

Address: 199.16.156.102

Address: 199.16.156.230

linux> nslookup twitter.com

Address: 199.16.156.102

Address: 199.16.156.230

Address: 199.16.156.6

Address: 199.16.156.70

某些合法的域名没有映射到任何IP地址:

linux> nslookup edu

*** Can't find edu: No answer

linux> nslookup ics.cs.cmu.edu

*** Can't find ics.cs.cmu.edu: No answer

IP地址与127.0.0.1的区别:127.0.0.1只能通过本地访问,IP地址既能通过本地访问也能通过外部访问。

 

三、因特网连接

一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做套接字对,由下列元组来表示:

(cliaddr:cliport, servaddr:servport)

 

四、套接字接口

1.套接字地址结构

 1 struct sockaddr_in
 2 {
 3     uint16_t sin_family;
 4     uint16_t sin_port;
 5     struct in_addr sin_addr;
 6     unsigned char sin_zero[8];
 7 };
 8 
 9 struct sockaddr
10 {
11     uint16_t sa_family;
12     char sa_data[14];
13 };

sin_family成员是AF_INET,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32位IP地址。IP地址和端口号总是以网络字节顺序存放的。

定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。

2.socket函数

客户端和服务器使用socket函数来创建一个套接字描述符。

1 #include <sys/types.h>
2 #include <sys/socket.h>
3 
4 // 若成功则为非负描述符,若出错则为-1。
5 int socket(int domain, int type, int protocol);

如果想要使套接字成为连接的一个端点,就用如下硬编码的参数来调用socket函数:

clientfd = Socket(AF_INET, SOCK_STREAM, 0);

AF_INET表明我们正在使用32位IP地址,而SOCK_STREAM表示这个套接字是连接的一个端点。

3.connect函数

客户端通过调用connect函数来建立和服务器的连接。

1 #include <sys/socket.h>
2 
3 // 若成功则为0,若出错则为-1。
4 int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);

connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到连接成功建立或是发生错误。

如果成功,clientfd描述符现在就准备好可以读写了,并且得到的连接是由套接字对(x:y, addr.sin_addr:addr.sin_port)刻画的,其中x表示客户端的IP地址,而y表示临时端口,它唯一地确定了客户端主机上的客户端进程。

4.bind函数

剩下的套接字函数——bind、listen和accept,服务器用它们来和客户端建立连接。

1 #include <sys/socket.h>
2 
3 // 若成功则为0,若出错则为-1。
4 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来。参数addrlen就是sizeof(sockaddr_in)。

5.listen函数

默认情况下,内核会认为socket函数创建的描述符对应于主动套接字,它存在于一个连接的客户端。

服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。

1 #include <sys/socket.h>
2 
3 // 若成功则为0,若出错则为-1。
4 int listen(int sockfd, int backlog);

listen函数将sockfd从一个主动套接字转化为一个监听套接字,该套接字可以接受来自客户端的连接请求。

backlog参数暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求的数量。

6.accept函数

服务器通过调用accept函数来等待来自客户端的连接请求。

1 #include <sys/socket.h>
2 
3 // 若成功则为非负连接描述符,若出错则为-1。
4 int accept(int listenfd, struct sockaddr *addr, int *addrlen);

accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符,

这个描述符可被用来利用Unix I/O函数与客户端通信。

监听描述符是作为客户端连接请求的一个端点。它通常被创建一次,并存在于服务器的整个生命周期。

已连接描述符是客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次接受连接请求时都会创建一个,它只存在于服务器为一个客户端服务的过程中。

 

四、主机和服务的转换

1.getaddrinfo函数

getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构。

它是已弃用的gethostbyname和getservbyname函数的新的替代品。和以前的那些函数不同,这个函数是可重入的,适用于任何协议。

 1 #include <sys/types.h>
 2 #include <sys/socket.h>
 3 #incldue <netdb.h>
 4 
 5 // 如果成功则为0,如果错误则为非零的错误代码。
 6 int getaddrinfo(const char *host, const char *service, 
 7                 const struct addrinfo *hints,
 8                 struct addrinfo **result);
 9 
10 void freeaddrinfo(struct addrinfo *result);
11 
12 // 返回错误信息
13 const char* gai_strerror(int errcode);

给定host和service(套接字地址的两个组成部分),getaddrinfo返回result,result指向一个addrinfo结构的链表,其中每个结构指向一个对应于host和service的套接字地址结构。

图 getaddrinfo返回的数据结构

在客户端调用了getaddrinfo之后,会遍历这个列表,依次尝试每个套接字地址,直到调用socket和connect成功,建立起连接。

服务器会尝试遍历列表中的每个套接字地址,直到调用socket和bind成功,描述符会被绑定到一个合法的套接字地址。

为了避免内存泄漏,应用程序必须在最后调用freeaddrinfo,释放该链表。

如果getaddrinfo返回非零的错误代码,应用程序可以调用gai_strerror,将该代码转换成消息字符串。

 1 struct addrinfo
 2 {
 3     int ai_flags;
 4     int ai_family;
 5     int ai_socktype;
 6     int ai_protocol;
 7     char *ai_canonname;
 8     size_t ai_addrlen;
 9     struct sockaddr *ai_addr;
10     struct addrinfo *ai_next;
11 }

getaddrinfo一个很好的方面是addrinfo结构中的字段是不透明的,即它们可以直接传递给套接字接口中的函数,应用程序代码无需再做任何处理。

ai_family、ai_socktype和ai_protocol可以直接传递给socket。ai_addr和ai_addrlen可以直接传递给connect和bind。

这个强大的属性使得我们编写的客户端和服务器能够独立于某个特殊版本的IP协议。

2.getnameinfo函数

getnameinfo函数和getaddrinfo是相反的,将一个套接字地址结构转换成相应的主机和服务名字符串。

它是已弃用的gethostbyaddr和getservbyport函数的新的替代品,和以前的那些函数不同,它是可重入和协议无关的。

1 #include <sys/socket.h>
2 #include <netdb.h>
3 
4 // 如果成功则为0,如果错误则为非零的错误代码。
5 int getnameinfo(const struct sockaddr *sa, socklen_t salen,
6                 char *host, size_t hostlen,
7                 char *service, size_t servlen, int flags);

getnameinfo函数将套接字地址结构sa转换成对应的主机和服务名字符串,并将它们复制到host和service缓冲区。

如果getnameinfo返回非零的错误代码,应用程序可以调用gai_strerror把它转化成字符串。

 1 #include "csapp.h"
 2 
 3 int main(int argc, char **argv)
 4 {
 5     struct addrinfo *p, *listp, hints;
 6     char buf[MAXLINE];
 7     int rc, flags;
 8 
 9     if (argc != 2)
10     {
11         fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
12         exit(0);
13     }
14 
15     /* Get a list of addrinfo records */
16     memset(&hints, 0, sizeof(struct addrinfo));
17     hints.ai_family = AF_INET;     /* IPv4 only */
18     hints.ai_socktype = SOCK_STREAM;     /* Connections only */
19     if (rc = getaddrinfo(argv[1], NULL, &hints, &listp) != 0)
20     {
21         fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
22         exit(1);
23     }
24 
25     /* Walk the list and display each IP address */
26     flags = NI_NUMERICHOST;     /* Display address string instead of domain name */
27     for (p = listp; p; p = p->ai_next)
28     {
29         getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
30         printf("%s\n", buf);
31     }
32 
33     /* Clean up */
34     freeaddrinfo(listp);
35 
36     exit(0);
37 }

这个程序称为HOSTINOF,它使用getaddrinfo和getnameinfo展示出域名到它相关联的IP地址之间的映射。该程序类似于NSLOOKUP程序。

 

五、套接字接口的辅助函数

1.open_clientfd函数

客户端调用open_clientfd建立与服务器的连接。

 1 #include "csapp.h"
 2 
 3 // 若成功则为描述符,若出错则为-1。
 4 int open_clientfd(char *hostname, char *port)
 5 {
 6     int clientfd;
 7     struct addrinfo hints, *listp, *p;
 8 
 9     /* Get a list of potential server addresses */
10     memset(&hints, 0, sizeof(struct addrinfo));
11     hints.ai_socktype = SOCK_STREAM;     /* Open a connection */
12     hints.ai_flags = AI_NUMERICSERV;      /* ... using a numeric port arg. */
13     hints.ai_flags |= AI_ADDRCONFIG;      /* Recommended for connections */
14     getaddrinfo(hostname, port, &hints, &listp);
15 
16     /* Walk the list for one that we can successfully connect to */
17     for (p = listp; p; p = p->ai_next)
18     {
19         /* Create a socket descriptor */
20         if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
21             continue;     /* Socket failed, try the next */
22 
23         /* Connect to the server */
24         if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
25             break;     /* Success */
26         close(clientfd);     /* Connect failed, try another */
27     }
28 
29     /* Clean up */
30     freeaddrinfo(listp);
31     if (!p)     /* All connects failed */
32         return -1;
33     else       /* The last connect succeeded */
34         return clientfd;
35 }

open_clientfd函数建立与服务器的连接,该服务器运行在主机hostname上,并在端口号port上监听连接请求。

它返回一个打开的套接字描述符,该描述符准备好了,可以用Unix I/O函数做输入和输出。

2.open_listenfd函数

调用open_listenfd函数,服务器创建一个监听描述符,准备好接收连接请求。

 1 #include "csapp.h"
 2 
 3 // 若成功则为描述符,若出错则为-1。
 4 int open_listenfd(char *port)
 5 {
 6     struct addrinfo hints, *listp, *p;
 7     int listenfd, optval = 1;
 8 
 9     /* Get a list of potential server addresses */
10     memset(&hints, 0, sizeof(struct addrinfo));
11     hints.ai_socktype = SOCK_STREAM;                       /* Accept connections */
12     hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;     /* ... on any IP address */
13     hints.ai_flags |= AI_NUMERICSERV;                       /* ... using port number */
14     getaddrinfo(NULL, port, &hints, &listp);
15 
16     /* Walk the list for one that we can bind to */
17     for (p = listp; p; p = p->ai_next)
18     {
19         /* Create a socket descriptor */
20         if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
21             continue;     /* Socket failed, try the next */
22         
23         /* Eliminates "Address already in use" error from bind */
24         setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, 
25                    (const void*)&optval, sizeof(int));
26 
27         /* Bind the descriptor to the address */
28         if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
29             break;     /* Success */
30         close(listenfd);     /* Bind failed, try the next */
31     }
32 
33     /* Clean up */
34     freeaddrinfo(listp);
35     if (!p)     /* No address worked */
36         return -1;
37 
38     /* Make it listening socket ready to accept connection requests */
39     if (listen(listenfd, LISTENQ) < 0)
40     {
41         close(listenfd);
42         return -1;
43     }
44     return listenfd;
45 }

open_listenfd函数打开和返回一个监听描述符,这个描述符准备好在端口port上接收连接请求。

转载于:https://www.cnblogs.com/xieguangzhong/p/7618453.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值