Linux网络编程(1)
Preview
课程要求,所以学了一下UNIX网络编程,老师说挺简单的,实际上手之后才发现这里面关系没那么简单。从CS:APP11章网络编程,再加上不停地man
,对当前的学习做个总结,也顺带当个报告了。
参考博客安利一下此重要参考,可能是国外程序员的习惯,这位大神段子层出不穷,讲的真的awesome
Some Important Data Structures
入手socket编程,第一个要理清的就是几个数据结构,出处是CS:APP
sockaddr
套接字地址指的是IP:PORT这样组合而成的结构,UNIX中利用sockaddr这个结构记录。需要注意的是,sockaddr是一个更通用的概念,如果借用类的概念类比的话,其实,他只是我们为了方便使用他的子类并保证函数的通用性而设计出来的,又因为以往的C未定义void指针,所以设计了此数据结构。这一概念而用于我们的网络编程的,是下面几个小节所提到的数据结构。
struct sockaddr{
unsigned short sa_family; // address family, AF_XXX
char sa_data[14]; // 14 bytes of protocol addresss
};
sa_family 指代的是 地址(A)家庭(F) 。例如AF_INET(IPv4), AF_INET(IPv6)
而sa_data是指的是套接字地址,即IP:PORT,并且格式化。
sockaddr_in
上面的sockaddr有个很明显的麻烦,sa_data的格式对于操作的程序员非常不友好,于是便有了接下来的细分。
IPv4所使用的的数据结构
struct sockaddr_in{
short int sin_family; // address family, here is AF_INET
unsigned short int sin_port; // Port
struct in_addr sin_addr; // Internet address
unsigned char sin_zero[8]; // Seted to keep the same size as struct sockaddr
};
这里的话就是将sockaddr细分,如此一来程序员也可以方便的操作记录套接字地址的数据结构。
其中sin_family就是AF_INET(这是记录IPV4的数据结构)。sin_port必须要转化为网络字节序,因此htons()
就派上了用场。
历史原因,sin_addr曾经是一个union
结构,参考博客里把他称为历史上的灾难...sin_zero是用来凑数的,这样此数据结构内存大小就和sockaddr一致了。
in_addr
此处有必要单独陈述sockaddr_in中提到的sin_addr,in_addr的定义如下
struct in_addr{
uint32_t s_addr;
};
sockaddr_in6
对应于IPV6的数据结构
struct sockaddr_in6{
u_int16_t sin6_family;
u_int16_t sin6_port;
u_int32_t sin6_flowinfo;
struct in6_addr sin6_addr;
u_int32_t sin6_scope_id;
};
新填的flow特性以及scope与IPV6新元素有关,此处先种树,之后知识齐全了,补全此处。
in6_addr
此处是对应于sockaddr_in 中的in_addr
struct in6_addr{
unsigned char s6_addr;
};
sockaddr_storage
这个兼容了,IPV4与IPV6的地址,然而迷惑之处是,他竟然比sockaddr的size要大
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
addrinfo
介绍完以上前置数据结构的细节,在此介绍addrinfo结构,结合之后的学习,其实addrinfo是一个“工具人”的角色,只是这个工具人几乎是human readable的套接字地址等概念与计算机格式化的、网络编程所利用的数据结构重要媒介
struct addrinfo{
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC(both 4&6)
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t addrlen; // sizeof ai_addr in bytes
struct sockaddr * ai_addr; //important information of addrinfo
char *ai_canonname;
struct addrinfo *ai_next;
}
其中ai_family, ai_socktype, ai_protocol是socket函数的第一、二、三个参数。
其他部分也各自有重要应用
How to manipulate these data structures?
这里我们就正式进入网络编程函数部分了,首当其冲的就是如何将人类可读的套接字地址表达式,转化为机器层面理解的、格式化的数据并填充到上述专有的数据结构。
Port Number
端口需要进行的转换在于字节序的转换,这部分就需要借助于htons()以及htonl()这两个函数,将输入目标端口号转换以后,再赋值给sockaddr_in(或sockaddr_in6)中的sin_port(或sin6_port)
IP Address
主要是机器可读以及人类可读的转化
先定义两个示例变量方便陈述
struct sockaddr_in sa;
struct sockaddr_in6 sa6;
那么人类可读到机器可读的函数可表示为
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr));
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr));
其中函数的第一个参数判定是IPV4还是IPV6,第二个就是IP地址,此处不可以是域名的格式,而必须写为数字加小数点的形式,结果存储到了sa.sin_addr(or sa6.sin6_addr)中。pton可以简单记为"printable to network"
机器可读转化为人类可读,就需要加上转化后字符串的 最大长度。
上例子
// IPv4:
char ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string
struct sockaddr_in sa; // pretend this is loaded with something
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string
struct sockaddr_in6 sa6; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);
INET_ADDRSTRLEN, INET6_ADDRSTRLEN是两个方便使用而设置的宏。
getaddrinfo()
真实网络编程中,每个数据结构的每一具体项手动填入实在是件很反人类的事情,而且这也增加了出错概率。
so借助getaddrinfo()的一些列方法组合安全方便。
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints,
struct addrinfo **res);
其中,host是域名或是IP地址。而service则是服务或是端口号,服务则会自动转化为对应周知端口再/etc/services文件中可以看到所有周知端口信息。
以下就可以对多个情况分析,如何转化套接字地址。
Server Address Convert
关于服务器的话模板式的套路如下,此针对TCP
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results, is a linklist
memset(&hints, 0, sizeof(hints)); // make sure hints is clean
hints.ai_family= AF_UNSPEC
hints.ai_socktype= SOCK_STREAM; // TCP stream sockets
hints.ai_flags= AI_PASSIVE; // fill in my IP for me
if (0!= (status= getaddrinfo(NULL, "3498", &hints, &servinfo))){
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return EXIT_FAILURE;
}
注意此处getaddrinfo()参数的设置:NULL。以及hints的设置,首先清空,ai_family以及ai_socktype进行必要声明后,AI_PASSIVE使得你不必手动填入本机的IP地址。当然,你也可以手动加入IP地址
另外一种方法用到了一个宏INADDR_ANY(IN6ADDR_ANY),直接设置sin_addr.s_addr(sin6_addr.s6_addr)。
Client Address Convert
对应的,客户端TCP示例
int status;
struct addrinfo hints;
struct addrinfo *servinfo;
memset(&hints, 0, sizeof(hints));
hints.ai_family= AF_UNSPEC;
hints.ai_socktype= SOCK_STREAM;
if (0!= (status= getaddrinfo("www.example.net", "3490", &hints, &servinfo))){
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return EXIT_FAILURE;
}
下面的示例程序可以看作一个域名转换,类似于nslookup
/*
** 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 *argv[])
{
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;
}
此篇介绍到此,下一篇blog将介绍TCP面向连接的操作