六星经典CSAPP-笔记(11)网络编程
2015-05-15 21:48
423 查看
六星经典CSAPP-笔记(11)网络编程
参照《深入理解计算机系统》简单学习了下Unix/Linux的网络编程基础知识,进一步深入学习Linux网络编程和TCP/IP协议还得参考Stevens的书。1.网络基础
(略过,待补充)2.IP地址
2.1 IP地址的表示
IP地址是一个无符号的32位整数。Linux网络程序使用下面这种IP地址结构存储IP地址:/* Internet address structure */ struct in_addr { unsigned int s_addr; /* Network byte order (big-endian) */ };
2.2 IP地址转换
因为网络中的主机有可能有不同的字节序,所以TCP/IP协议定义了一种统一的网络字节序Network Byte Order,即大尾端字节序(基础知识请参考六星经典CSAPP笔记(2)信息的操作和表示)。这样不论主机是大尾端还是小尾端,网络传输时在数据包的header中保存的IP地址都是网络字节序(大尾端)的,实现了程序的可移植性。为了方便这种操作,Unix提供了一些库函数供我们调用。
System Call | Function |
---|---|
unsigned long int htonl(unsigned long int hostlong) | Convert a 32-bit integer from host byte order to network byte order |
unsigned short int htons(unsigned short int hostshort) | Convert a 16-bit integer from host byte order to network byte order |
unsigned long int ntohl(unsigned long int netlong) | Convert a 32-bit integer from network byte order to host byte order |
unsigned short int ntohs(unsigned short int netshort) | Convert a 16-bit integer from network byte order to host byte order |
int inet_aton(const char *cp, struct in_addr *inp) | Convert a dotted-decimal string (cp) to an IP address in network byte order (inp) |
char *inet_ntoa(struct in_addr in) | Convert an IP address in network byte order to its corresponding dotted-decimal string |
netinet/in.h头文件中,用来在主机字节序和网络字节序间转换。后两个函数包含在
arpa/inet.h中,用来在IP地址字符串和in_addr数据结构间转换。下面来看一个小例子:
#include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <netinet/in.h> int main(int argc, char const *argv[]) { struct in_addr addr; char *addr_str; uint32_t net_ip; addr_str = "192.168.1.100"; if (inet_aton(addr_str, &addr) == 0) { printf("Convert %s to in_addr failed\n", addr_str); exit(1); } // Convert 192.168.1.100 to host byte order 0x6401a8c0 printf("Convert %s to host byte order 0x%08x\n", addr_str, addr.s_addr); // Convert host byte order 0x6401a8c0 to network byte order 0xc0a80164 net_ip = htonl(addr.s_addr); printf("Convert host byte order 0x%08x " "to network byte order 0x%08x\n", addr.s_addr, net_ip); addr.s_addr = net_ip; return 0; }
apra/inet.c中的ARPA是什么?CSAPP中讲到了Internet的历史,1957年冷战时埋下了互联网的种子,当时苏联发射了第一颗人造地球卫星Sputnik震惊了世界(村上的小说《斯普特尼克恋人》就是指它)。作为回应,美国政府创建了Advanced Research Projects Agency,即ARPA,想要重新建立科技的领导地位。于是1969年一个全新的网络建立起来,叫做ARPANET,也就是互联网的前身。
通过inet_aton函数将字符串192.168.1.100转换成了in_addr数据结构,其s_addr整数值为0x6401a8c0。简单分析一下:
0x64=100,0x01=1,0xA8=168,0xC0=192
说明我的机器是小尾端字节序。之后再使用htonl函数将小尾端字节序的IP地址转为统一的网络字节序。下面是一道课后练习题,编写一个dd2hex.c小工具,将用户输入的IP地址字符串转成网络字节序的16进制整数。
#include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <netinet/in.h> /** * Usage: * unix> gcc dd2hex * unix> ./dd2hex 128.2.194.242 * 0x8002c2f2 * * @param argc 2 * @param argv 128.2.194.242 * @return 0x8002c2f2 */ int main(int argc, char const *argv[]) { struct in_addr addr; char const *addr_str; uint32_t net_ip; // 1.Get input arg from cmd line if (argc < 1) { printf("Usage: dd2hex ip-address-string\n"); exit(1); } addr_str = argv[1]; // 2.Convert to integer in host byte order if (inet_aton(addr_str, &addr) == 0) { printf("Convert %s to in_addr failed\n", addr_str); exit(1); } // 3.Convert to network byte order net_ip = htonl(addr.s_addr); printf("0x%08x\n", net_ip); return 0; }
2.网络域名
网络上的主机使用IP地址通信,然而IP地址对于人类是“不友好”的,难于记忆,所以就有了域名Domain Name。最初域名和IP地址的映射关系是保存在文本文件HOSTS.txt中由人工维护的。随后出现了全球性的分布式数据库DNS(Domain Name System),以host entry的形式保存映射关系,Unix中的数据结构是:/* DNS host entry structure */ struct hostent { char *h_name; /* Official domain name of host */ char **h_aliases; /* Null-terminated array of domain names */ int h_addrtype; /* Host address type (AF_INET) */ int h_length; /* Length of an address, in bytes */ char **h_addr_list; /* Null-terminated array of in_addr structs */ };
为什么hostent中的h_addr_list是char**而不是struct in_addr**呢?在StackOverflow上的一篇问答中一位老兄给出了解释:
hostent是个古老的struct定义,甚至早于void*的出现。在那时,char *被当成万能指针使用。如今理应改成void **而不是char **,但为时已晚!
那时有很多网络协议,作者不知道哪种协议会成为主流。即使在TCP/IP称为主宰的今天,h_addr_list仍然有两种可能,包含struct in_addr *或struct in6_addr *。
网络程序可以从DNS数据库中取出任意的host entry。Unix在
netdb.h头文件中提供了API:
System Call | Function |
---|---|
struct hostent *gethostbyname(const char *name) | Return the host entry associated with the domain name name |
struct hostent *gethostbyaddr(const char *addr, int len, 0) | Return the host entry associated with the IP address addr |
确定输入是域名还是IP:利用inet_aton返回0还是1(转换IP地址字符串到整数是否成功)确定用户输入的到底是域名还是IP地址。
处理错误码:当域名或IP地址无效时,gethostbyaddr/name会返回NULL,这时错误码保存在h_errno中,可以用hstrerr取出对应的错误提示消息。
别名是字符串数组:entry中的h_alias是个字符串数组,因为域名可以对应好多个别名。
IP地址是字符串数组:同样,一个域名也可能对应多个IP地址,所以就达到了DNS负载均衡的效果。课后练习题说连续运行hostinfo google.com三次,会看到域名对应的IP地址顺序发生变化。
h_addr_list是char* :entry->h_addr_list中的每个值都是char*,使用时被转成了struct in_addr *,具体原因参见前面的解释。
#include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <netdb.h> /** * Usage: * $ ./hostinfo.exe www.baidu.com * Official hostname: www.a.shifen.com * Alias: www.baidu.com * Address: 220.181.112.244 * Address: 220.181.111.188 * * @param argc 2 * @param argv www.baidu.com or 220.181.112.244 * @return Official hostname/Alias/Address */ int main(int argc, char const *argv[]) { char **host; struct in_addr addr; struct hostent *entry; if (argc != 2) { fprintf(stderr, "Usage: %s <domain name or dotted-decimal>\n", argv[0]); exit(1); } // Retrieve host entry by "domain name"/"IP address string" from DNS if (inet_aton(argv[1], &addr)) entry = gethostbyaddr((char *)&addr, sizeof(addr), AF_INET); else entry = gethostbyname(argv[1]); if (entry == NULL) { fprintf(stderr, "Error: %s\n", hstrerror(h_errno)); exit(1); } printf("Official hostname: %s\n", entry->h_name); for (host = entry->h_aliases; *host; host++) printf("Alias: %s\n", *host); for (host = entry->h_addr_list; *host; host++) { addr.s_addr = ((struct in_addr *) *host)->s_addr; printf("Address: %s\n", inet_ntoa(addr)); } return 0; }
3.Socket接口
3.1 Socket地址
客户端和服务器通过一条全双工、可靠的连接(full-duplex reliable connection)发送和接收数据流。连接的两端是两个Socket组成的二元组,Socket地址是由IP地址:端口号组成。其中客户端的端口号是由操作系统内核自动分配的临时端口(ephemeral port),而服务器端则是个永久的端口号(well-known port)。下面来看一下Socket的数据结构,在
socket.h中包含了sockaddr,
netinet/in.h中包含了sockaddr_in的定义:
/* Generic socket address structure (for connect, bind, and accept) */ struct sockaddr { unsigned short sa_family; /* Protocol family */ char sa_data[14]; /* Address data. */ }; /* Internet-style socket address structure */ struct sockaddr_in { unsigned short sin_family; /* Address family (always AF_INET) */ unsigned short sin_port; /* Port number in network byte order */ struct in_addr sin_addr; /* IP address in network byte order */ unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */ };
那时的C语言没有void *类型,为了适应不同的网络地址类型,Socket的connect、bind、accept等各种函数都用sockaddr作为参数,应用代码需要将特定协议的struct指针转为通用的sockaddr指针。从上面的struct定义也能看出,sockaddr_in结构的末尾添加了8个字节的padding来匹配sockaddr的长度。
3.2 API纵览
Socket接口是一组与Unix I/O接口配合建立起网络应用的函数集,它在客户端与不同协议实现之间建立起一个抽象层。下面这张图很重要,会作为学习Unix/Linux Socket编程的路线图:3.3 客户端连接
bind,listen,accept函数都是服务端要使用的,客户端的步骤很简单,主要关注socket和connect两个函数。下面来看一下客户端的代码,主要由以下三步组成:创建sockaddr_in:根据IP地址in_addr和端口号组成。
调用socket():创建套接字,用Unix的描述符表示。
调用connect():用前两步得到的sockaddr和socket描述符开启连接。
/** * Open connection by socket in client-side * @param hostname host name or IP address * @param port port * @return descriptor */ int open_clientfd(char *hostname, int port) { struct hostent *host; struct sockaddr_in sockaddr; int clientfd; // 1.Comine IP address and port as socket address memset(&sockaddr, 0, sizeof(sockaddr)); sockaddr.sin_family = AF_INET; sockaddr.sin_port = htons(port); // 2.Parse IP address from hostname if (inet_aton(hostname, &sockaddr.sin_addr) == 0) { if ((host = gethostbyname(hostname)) == NULL) { fprintf(stderr, "Error: %s\n", hstrerror(h_errno)); return -1; } memcpy(&sockaddr.sin_addr.s_addr, host->h_addr_list[0], host->h_length); } // 3.Create socket if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); return -2; } // 4.Establish a connection to server if (connect(clientfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); return -3; } return clientfd; }
提一下几个实现上的小细节:
内存初始化和拷贝:可以用bzero/bcopy或memset/memcpy。这是两套API,前者是POSIX标准的,后者是C语言标准的。后者包含在memory.h或string.h中。
错误码处理:Socket函数的错误都是放在标准错误码errno中,使用strerror取出错误提示消息,用法与gethostbyname的h_errno和hstrerror完全一样。
3.4 服务端监听
服务端的代码稍微复杂一点点,主要分为四步:创建sockaddr_in:使用INADDR_ANY告诉内核此服务器程序能够接受来自任何IP地址的请求。
调用socket():创建套接字,用Unix的描述符表示。
调用bind():将前两步得到的sockaddr和socket描述符绑定到一起。
调用listen():开始监听请求。
/** * Listen on socket for incoming request in server-side. * @param port listen port * @return descriptor */ int open_serverfd(int port) { int listenfd, optval = 1; struct sockaddr_in sockaddr; // 1.Create socket address memset(&sockaddr, 0, sizeof(sockaddr)); sockaddr.sin_family = AF_INET; sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); sockaddr.sin_port = htons(port); // 2.Create socket of specific protocal if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); return -1; } // 3.Eliminates "Address already in use" error from bind if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int)) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); return -2; } // 3.Bind socket and address if (bind(listenfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); return -3; } // 4.Start to listen for connection requests with backlog queue 10 if (listen(listenfd, 10) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); return -4; } return listenfd; }
3.5 数据通信
有了上面两个工具函数,接下来就能写出完整的客户端和服务器通信代码了。测试方法是先./socket server 7777运行服务端,再./socket client “localhost” 7777 “helloworld”运行客户端。服务端的核心在于启动监听后循环accept()客户请求:// printf, fprintf... #include <stdio.h> #include <stdlib.h> // memset in string/memory.h #include <string.h> // strerror #include <errno.h> // in_addr/hostent/sockaddr, htons, gethost #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> // sockaddr_in, connect/bind/accept #include <sys/types.h> #include <sys/socket.h> // close, write, read #include <unistd.h> // constants #define MAXLINE 100 #define FAILURE 1 // prototypes int open_clientfd(char *hostname, int port); int open_serverfd(int port); int main(int argc, char const *argv[]) { /* * 1.Check arguments: * prog server <port> <msg> * prog client <hostname> <port> <msg> */ if (argc <= 2 || ((argc == 3) && strcmp(argv[1], "server")) || (argc == 4) || ((argc == 5) && strcmp(argv[1], "client")) || argc >= 6) { fprintf(stderr, "Usage: server <port> or " " client <host> <port> <msg>\n"); exit(FAILURE); } if (!strcmp(argv[1], "client")) { char *hostname; int port; int clientfd; char *msg; hostname = argv[2]; port = atoi(argv[3]); msg = argv[4]; // 2.Connect to remote server if ((clientfd = open_clientfd(hostname, port)) < 0) exit(FAILURE); // 3.Send messge if (send(clientfd, msg, strlen(msg), 0) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); exit(FAILURE); } // 4.Write echo message to stdout char buf[MAXLINE]; int readsize; if ((readsize = recv(clientfd, buf, MAXLINE, 0)) > 0) puts(buf); // 5.Close connection close(clientfd); } else { int listenfd, connfd, socklen; int port; struct sockaddr_in sockaddr; port = atoi(argv[2]); // 2.Listen for incoming request if ((listenfd = open_serverfd(port)) < 0) exit(FAILURE); while (1) { // 3.Accept a new request if ((connfd = accept(listenfd, (struct sockaddr *)&sockaddr, &socklen)) < 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); exit(FAILURE); } printf("New client from %s\n", inet_ntoa(sockaddr.sin_addr)); // 4.Echo back what client just sent char buf[MAXLINE]; int readsize; while ((readsize = recv(connfd, buf, MAXLINE, 0)) > 0) write(connfd, buf, strlen(buf)); memset(buf, 0, strlen(buf)); // 5.Close connection close(connfd); } close(listenfd); } return 0; }
为什么要有listenfd和connfd两个Socket描述符?有什么区别?
listenfd只会创建一次,与服务器生命周期相同;而connfd则是每个客户端与服务器建立起连接时都会创建。直觉上感觉这样做有点复杂,不够简洁,但这样区分开来的好处是:可以轻松构建起能同时处理很多客户端连接的并发服务器。例如,每次accept()后都fork()一个新进程处理。
相关文章推荐
- 六星经典CSAPP-笔记(11)网络编程
- 六星经典CSAPP-笔记(10)系统IO
- 六星经典CSAPP-笔记(10)系统IO
- 六星经典CSAPP-笔记(12)并发编程(上)
- Java基础知识强化之网络编程笔记11:TCP之TCP协议上传文本文件
- 六星经典CSAPP-笔记(12)并发编程(上)
- java网络编程 tcp 黑马程序员学习笔记(11)
- [CSAPP笔记][第十一章网络编程]
- 六星经典CSAPP笔记(1)计算机系统巡游
- JAVA网络编程叶存菜鸟TCP程序设计笔记echo程序经典案列
- 六星经典CSAPP笔记(1)计算机系统巡游
- 六星经典CSAPP-笔记(7)加载与链接(上)
- [置顶] [CSAPP笔记][第十一章网络编程]
- 六星经典CSAPP-笔记(3)程序的机器级表示
- 六星经典CSAPP-笔记(7)加载与链接(上)
- 六星经典CSAPP-笔记(3)程序的机器级表示
- Linux笔记 11 -- 网络编程
- 六星经典CSAPP-笔记(12)并发编程(上)
- linux c 笔记 网络编程(三)..套接字数据传输
- JavaSE学习笔记之网络编程