- 英文小册原文地址:beej.us/guide/bgnet…
- 作者:Beej
- 中文翻译地址:www.chanmufeng.com/posts/netwo…
现将目录贴下:
- 什么是socket
- 两种Socket
- 漫谈网络
- IP地址、struct以及地址转换
- IP地址,IPv4和IPv6
- 子网
- 字节序
- socket相关的数据结构
- 再谈IP地址
- 从IPv4迁移到IPv6
- System call
- getaddrinfo()—准备开始!
- socket()—拿到文件描述符!
- bind()—我在监听哪个端口?
- connect()—嘿,你好啊!
- listen()—会有人联系我吗?
- accept()—感谢呼叫3490端口
- send() and recv()—跟我唠唠吧,宝儿!
- sendto() and recvfrom()—用DGRAM风格跟我说话
- close() and shutdown()—滚犊子!
- getpeername()—你哪位?
- gethostname()—我是谁?
- Client-Server基础
- 一个简单的流服务器
- 一个简单的流客户端
- Datagram Sockets
- 技术进阶
- Blocking—何谓阻塞?
- poll()—同步的I/O多路复用
- select()—老古董的同步I/O多路复用
- 数据只传了一部分怎么办?
- Serialization-如何封装数据?
- 数据封装
- 广播数据包-大声说「Hello,World」
这一章,我们将走进系统调用(System Call),这些系统调用允许我们访问Linux设备或其他任何支持Socket API设备(比如BSD、Windows、Linux、Mac等)的网络功能。当你调用某个系统调用时,内核将接管一切,自动为你处理接下来的一切任务。
多数人觉得网络编程很难是因为不知道什么时候该调用什么函数,这个问题在man手册里是找不到解决方案的。为了带大家走出困境,接下来我就按照一般的调用顺序来给大家讲解每一个系统调用,你在写程序的时候就按照这个顺序调用就行了。
要想看懂接下来散落各处的代码片段,你需要备点牛奶和饼干了,还多少需要备点决心和勇气,然后你就能将socket编程玩儿得风生水起了,不知道的还以为你得了Jon Postel真传呢!
Jon Postel是互联网的巨擘,他的发明作品包括但不限于SMTP协议
、FTP协议
、UDP协议
。
需要说明的是,为了简洁,很多代码片段并没有包含必要的错误检查。比如文章会始终假设getaddrinfo()
会调用成功。在编写生产环境代码时尤其需要注意这一点。
这是socket编程中非常重要的一个函数,它有非常多的参数,但是别害怕,用起来其实非常简单。这个函数的作用是帮你设置之后需要的struct。
稍微提一嘴它的历史:它的前身是用来做DNS查询的gethostbyname()
,当时还需要你手动把数据设置到struct sockaddr_in
中呢。谢天谢地,现在不用了。
使用getaddrinfo()
可以帮助你写出兼容IPv4和IPv6的代码,甚至在帮你进行DNS查询和service name查询之后,还会自动将你需要的信息填充到struct
中,咱就说多牛!
看一眼长啥样先。
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints,
struct;
你传给它3个参数(其实是4个,但是第4个只是个声明的变量而已),它给你返回一个指向链表的指针res
。
其中,node
参数是你想进行连接的服务器的域名,或者IP地址。
service
参数可以是个端口号,比如“80
”,或者是一个特定服务的名称,比如“http
”、“ftp
”、“telnet
”、“smtp
” 等。
最后,hints
参数指向了一个需要你设置相关参数的struct addrinfo
。
常用端口号以及服务名可以参见 The IANA Port List或者Unix设备上的/etc/services
文件
接下来给一个服务端程序监听本机IP地址和3490
端口的例子。需要注意的是,代码示例中并没有做监听(listen)和任何网络设置的工作,只是简单设置了之后会用到数据结构而已。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
// servinfo 现在指向了包含1个或多个addrinfo结构的链表
// ... do everything until you don't need servinfo anymore ....
freeaddrinfo(servinfo); // free the linked-list
注意,我将ai_family
设置成了AF_UNSPEC
,也就意味着我压根不在意我们用IPv4还是IPv6。如果你想精确指定IPv4或者IPv6的话请使用AF_INET
或AF_INET6
。
你可能注意到了,我把ai_flags
设置为了AI_PASSIVE
,意思是告诉getaddrinfo()
函数把我本机的IP设置到servinfo
中,这样我就不用将getaddrinfo()
的第一个参数(如果忘了是啥意思,看看上文)硬编码了,直接设置成NULL
就行了。
然后我们就调用getaddrinfo()
了。如果函数报错(返回值不为0),我们会使用gai_strerror()
函数将错误打印出来。如果程序正常运行,那么servinfo
最终就会指向一个由struct addrinfo
链接形成的链表,链表中每一个元素都会包含一个我们之后会用到的struct sockaddr
。
最后,getaddrinfo()
会在堆内存中创建servinfo
指向的链表,使用完之后一定要使用freeaddrinfo()
进行内存释放。
再给一个客户端代码的例子,我们假设客户端想连接到域名为“www.chanmufeng.com”,端口为3490
的服务器。再次强调,代码片段中省略了实际进行链接的部分,只是简单设置了之后会用到数据结构而已。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
// get ready to connect
status = getaddrinfo("www.chanmufeng.com", "3490", &hints, &servinfo);
// servinfo now points to a linked list of 1 or more struct addrinfos
// etc.
这段代码比server端的代码还简单,就不过多解释了。
接着我们综合使用一下我们学过的知识,写一段稍微长一丢丢的demo,这个demo的作用是打印你在命令行上指定的任何主机的IP地址。
/*
** showip.c -- show IP addresses for a host given on the command line
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char
{
struct addrinfo hints, *res, *p;
int status;
char ipstr[INET6_ADDRSTRLEN];
if (argc != 2) {
fprintf(stderr,"usage: showip hostname\n");
return 1;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
hints.ai_socktype = SOCK_STREAM;
if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 2;
}
printf("IP addresses for %s:\n\n", argv[1]);
for(p = res;p != NULL; p = p->ai_next) {
void *addr;
char *ipver;
// get the pointer to the address itself,
// different fields in IPv4 and IPv6:
if (p->ai_family == AF_INET) { // IPv4
struct sockaddr_in *ipv4 (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "IPv4";
} else { // IPv6
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
}
// convert the IP to a string and print it:
inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
printf(" %s: %s\n", ipver, ipstr);
}
freeaddrinfo(res); // free the linked list
return 0;
}
程序使用你在命令行中输入的参数来调用getaddrinfo()
,然后我们就得到了res
指向的链表,遍历链表每个节点我们就能得到全部的IP信息。
运行方式如下:
$ showip www.example.net
IP addresses for www.example.net:
IPv4: 192.0.2.88
$ showip ipv6.example.com
IP addresses for ipv6.example.com:
IPv4: 192.0.2.101
IPv6: 2001:db8:8c00:22::171
现在一切尽在我们掌握之中了,我们可以将getaddrinfo()
自动为我们填充好的数据传递给其他的socket函数,这就是把getaddrinfo()
函数放在第一位进行介绍的原因了。
更多socket系统调用,请期待下篇。