1、通用socket地址:
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
1
2
3
4
5
6
|
# include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[ 14 ]; } |
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family)和对应的地址族如表:
协议族 | 地址族 | 描述 |
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPV4协议族 |
PF_INET6 | AF_INET6 | TCP/IPV6协议族 |
宏PF_*和宏AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度。
PF_UNIX | 文件的路径名,长度可达到108字节 |
PF_INET | 16位端口号和32位ipv4地址,共6字节 |
PF_INET6 | 16位端口号和32位流标识,128位ipv6地址,32位范围id,共26字节 |
协议族 | 地址值含义和长度 |
2、专用socket编程。
TCP/IP协议族中有sockaddr_in和ascoaddr_in6两个专用socket地址结构体,他们分别用于ipv4
和ipv6:
1
2
3
4
5
6
|
# include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; //地址族:AF_UNIX char sun_path[ 108 ]; //文件路径名 } |
1
2
3
4
5
6
7
8
9
10
|
struct socketaddr_in { sa_family_t sin_family; //地址族:AF_INET u_int16_t sin_port; //端口号,要用网络字节序表示 struct in_addr sin_addr; //ipv4地址结构体 } struct in_addr { u_int32_t s_addr; //ipv4地址,要用网络字节序表示 } |
1
2
3
4
5
6
7
8
|
struct sockaddr_in6 { sa_family_t sin6_family; //地址族:AF_INET6 u_int16_t sin6_port; //端口号 u_int32_t sin6_flowinfo; //流信息,因该设置为0 struct in6_addr sin6_addr; //ipv6地址结构体 u_int32_t sin6_scope_id; //scope id ,尚处于试验阶段 } |
这两个专用地址结构体个字段的含义都很明确,我们只在右边稍加注释。
所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转化即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。
3、ip地址转换函数。
通常,人们习惯使用可读性好的字符串来表示ip地址,比如用点分十进制字符串表示ipv4地址,以及用16进制来表示ipv6地址。但编程中我们需要先把他们转化为整数二进制数方能使用。而记录日志时则相反,我们要把整数表示的ip地址转化为可读的字符串。下面三个函数可用于用点分十进制字符串表示的ipv4地址和用网络字节序整数表示的ipv4地址之间的转换:
1
2
3
4
|
# include <arpa/inet.h> in_addr_t inet_addr( const char * strptr); int inet_aton( const char *cp,struct in_addr* inp); char *inet_ntoa(struct in_addr in ); |
inet_addr函数将用点分10进制字符串表示的ipv4地址转化为用网络字节序整数来表示的ipv4地址。他失败时则返回INADDR_NONE。
inet_aton函数完成和inet_addr相同的功能,但是将转化结果存储于参数inp指向的地址结构中。成功时返回1,失败则返回0。
inet_ntoa函数将用网络字节序整数表示的ipv4地址转化为用点分十进制字符串表示的ipv4地址。但要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。
1
2
3
4
|
char * szValue1=inet_ntoa( "1.2.3.4" ); char * szValue2=inet_ntoa( "10.194.71.60" ); printf( "address 1:%s" ,szValue1); printf( "address 2:%s" ,szValue1); |
结果是:address1:10.194.71.60
address2:10.194.71.60
下面这对跟新的函数也能完成和前面3个函数同样的功能,并且他们同时适用于ipv4地址和ipv6地址:
1
2
3
|
# include <arpa/inet.h> int inet_pton( int af, const char *src, void * dst); const char *inet_ntop( int af, const void * src,char *dst,socklen_t cnt); |
inet_pton函数将用字符串表示的ip地址src(用点分十进制字符串表示的uov4地址或用16进制字符串表示的ipv6地址)转换成用网络字节序整数表示的ip地址,并把转换结果存储于dst指向的内层中。其中,af参数指定地址族,可以是AF_INET或者AF_INET6。inet_pton成功时返回1,失败则返回0并设置errno。
inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定目标指定目标存储单元的大小。下面的两个宏能帮助我们制定这个大小(分别用ipv4和ipv6):
1
2
3
|
# include <netinet/ in .h> #define INET_ADDRSTRLEN 16 ; #define INET6_ADDRSTRLEN 46 ; |
inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno。
1、创建socket。
linux的一个哲学是:所有东西都是文件。socket也不列外,他就是可读,可写,可控制,可关闭的文件描述符。下面的socket系统调用可创建的一个socket:
1
2
3
|
# include <sys/types.h> # include <sys/socket.h> int socket( int domain, int type, int protocol); |
domain告诉系统使用的是那个底层协议族。对TCP/IP协议族而言,该参数应该设置为PF_INET(用于ipv4)或PF_INET6
type参数指定服务类型。服务类型主要有sock_stream服务(流服务)和sock_dgram(数据报)服务。对TCP/IP协议族而言,其值取sock_stream表示传输层使用tcp协议,取sock_dgram表示传输层使用udp协议。
protocol参数是在前两个参数构成的协议集合下,在选择一个具体的协议。不过这个值通常是唯一有的。几乎所有情况下,我们都应该把它设置为0,表示默认协议。
socket系统调用成功时返回一个sicket文件描述符,失败则返回-1并设置errno。
2、命名socket
创建socket时,我们给他指定了地址族,但是并未指定使用该地址族中的哪个具体的socket地址。讲一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常要命名socket,因为只有命名之后的客户端才能知道该如何连接它。客户端通常不需要命名socket,而是采用匿名模式,即使使用操作系统自动分配的socket地址。命名socket的系统调用的是bind,定义如下:
1
2
3
|
# include <sys/types.h> # include <sys/socket.h> int bind( int sockfd, const struct sockaddr* myaddr,socklen_t addrlen) |
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。
bind成功时返回0,失败则返回-1并设置errno。其中两种常见的哦errno是EACCES和EADDRINUSE,他们的含义分别是:
EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口上时,bind将返回eacces错误。
EADDRINUSE:被绑定的地址正在被使用中。比如将socket绑定到一个处于TIME_wait状态的socket地址。
3、监听socket
scket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
1
2
|
# include <sys/socket.h> int listen( int sockfd, int backlog); |
sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将受到ECONNREFUSED错误信息。
listen成功时返回0,失败则返回-1并设置errno。
该服务器程序接收3个参数:ip地址,端口号,和backlog值。
4、接受连接:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t * addrlen);
sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接socket,gaisocket唯一地表示了被接受的这个连接,服务器可通过读写该socket来与被接受连接的客户端通信。accept失败时则返回-1,并设置erron。
现在考虑如下情况:如果监听队列中处于ESTABLISHED状态的连接对应的客户端出现异常(比如掉线),或者提前退出,那么服务器对这个连接执行的accept调用是否成功?
5、发起连接
如果说服务器通过listen调用来被动的接受连接,那么客户端需要通过如下系统函数来调用服务器函数。
6、关闭连接。
关闭一个连接实际上就是关闭该连接对应的socket。