1、基本结构
大多数套接口函数都需要一个指向套接口地址结构的指针作为参数。每个协议族都定义它自己的套接口地址结构。这些结构的名字均以“sockaddr_”开头,并以对应每个协议族的唯一后缀结束。
1.1 IPv4套接口地址结构
也称为“网际套接口地址结构”,以“sockaddr_in”命名,在头文件<netinet/in.h>中。
struct in_addr { in_addr_t s_addr; //32为IP地址 }; struct sockaddr_in { uint8 sin_len; //为了增加OSI协议支持,有些版本是没有这个字段的,一般不用 sa_family_t sin_family;//类型为u_short,IPv4版本的值为AF_INET in_port_t sin_port;//16位整数 struct in_addr sin_addr;//地址结构,可以为32位整数,有些版本运行访问每个字节 char sin_zero[8]; //暂时无用 };
注意:
一般情况下,以上结构中只使用sin_family、sin_addr、sin_port三个成员,几乎所有版本实现都会增加sin_zero字段,所以所有套接口地址结构都至少为16字节。
IPv4地址和TCP或UDP端口号在套接口地址结构中总是以网络字节序来存储。
可以有两种不同方法来访问IPv4地址,假设serv为一个网际套接口地址,那么可以用serv.sin_addr(一个in_addr结构)和serv.sin_addr.s_addr(一个32位整数)两种形式来访问IPv4地址。现在大多数情况下使用32位整数的形式来访问。
1.2 通用套接口地址结构
当以参数形式传递给套接口函数时,套接口结构总是通过指针来传递,这样就会有一个问题,如何来声明所传指针的数据类型?这样通用套接口就派上用场了。
其实用void *类型很容易就解决这个问题了,但套接口函数在标准C之前定义的,当时采用了如下结构:(在头文件sys/socket.h中)
struct sockaddr { uint8_t sa_len; sa_family_t sa_family; //AF_xxx char sa_data[14]; };
例如,bind函数的原型为:
int bind(int , struct sockaddr *, socklen_t );
这样就要求使用的时候需要将套接口结构指针强转成该类型。
1.3 IPv6套接口地址结构
在头文件netinet/in.h中定义。
struct in6_addr { unit8_t s6_addr[16]; //128位地址 }; #define SIN6_LEN struct sockaddr_in6 { uint8_t sin6_len; sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; };
注意:
如果系统支持长度成员,则SIN6_LEN常量必须定义。
IPv6地址族是AF_INET6
sin6_flowinfo成员分为三个字段:
低24位是流量标号;
下4位是优先级;
再下4位保留;
1.4 值-结果参数
当把套接口地址结构传递给套接口函数时,总是通过指针来传递的。现在结构的长度也作为参数传递,其传递的方式取决于结构传递方向:从进程到内核与从内核到进程。从进程到内核传递整数长度,从内核到进程传递整型指针。
从进程到内核传递套接口结构的有三个函数:bind、connect、sendto。由于指针和指针所指向结构的大小都传递给内核,所以从进程到内核需要拷贝多少数据是已知的。
从内核到进程有四个函数:accept、recvfrom、getsockname、getpeername。传长度是传递指向整数的指针。
为什么要传指针呢?因为:当函数被调用时,结构大小是一个值(告诉内核该结构的大小,使内核在写此结构时不越界),当函数返回时,结构大小又是一个结果(它告诉进程,内核在此结构中确切存储了多少信息),这种参数类型叫值-结果参数。
其他值-结果参数:
select函数中间的三个变量;
getsockopt函数的长度变量;
使用函数recvmsg时,msghdr结构中的两个成员:msg_namelen和msg_controllen;
ifconf结构中的成员ifc_len;
sysctl函数的前两个长度参数。
2、基本接口
2.1 如何判断系统大小端
int main(int argc, char *argv[]) { uinon { short s; char c[sizeof(short)]; } un; un.s = 0x0102; printf("this os is: "); if (sizeof(short) == 2) { if (un.c[0] == 1 && un.c[1] == 2) printf("big-endian\n"); else if (un.c[0] == 2 && un.c[1] == 1) printf("little-endian\n"); else printf("unknown\n"); } else { printf("sizeof(short) = %d\n", sizeof(short)); } return 0; }
2.2 各种系统函数介绍
2.2.1 htons和htonl,用于将主机字节序转化为网络字节序
-----------------------------------------------------------------
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue);//返回网络字节序值
uint32_t htonl(uint32_t host32bitvalue);//返回网络字节序值
------------------------------------------------------------------
传入16或32为主机字节序值,返回相应的网络字节序值。
2.2.2 ntohs和ntohl,用于将网络字节序转化为主机字节序
------------------------------------------------------------------
#include <netinet/in.h>
uint16_t ntohs(uint16_t net16bitvalue);//返回主机字节序值
uint32_t ntohl(uint32_t net32bitvalue);//返回主机字节序值
------------------------------------------------------------------
传入16或32为网络字节序值,返回相应的网络字节序值。
2.2.3 bzero、bcopy、bcmp,相当于C语言中的memset、memcpy、memcmp
------------------------------------------------------------------
#include <strings.h>
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *prt2, size_t nbytes); //返回0相等,非0不等
------------------------------------------------------------------
------------------------------------------------------------------
#include <string.h>
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);//返回0相等,非0不等
------------------------------------------------------------------
2.2.4 inet_aton,将一个字符串IP地址转换为一个32位的网络字节序地址。
------------------------------------------------------------------
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);//返回1有效,返回0无效
------------------------------------------------------------------
参数strptr为点分式IP字符串,addrptr为转化后的in_addr格式地址。
2.2.5 inet_addr,将一个点分式IP转化为一个32位整数
------------------------------------------------------------------
#include <arpa/inet.h>
in_addr_t inet_addr(const char *strptr);//返回32位整数,若地址非法,返回INADDR_NONE
------------------------------------------------------------------
参数strptr为点分式IP字符串。
INADDR_NONE等于255.255.255.255(IPv4的有限广播地址),所以该函数不能处理此地址。尽量使用inet_aton,不使用inet_addr。
2.2.6 inet_ntoa,将一个32位网络字节序地址转换为一个字符串IP地址。
------------------------------------------------------------------
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr inaddr);//返回点分式IP字符串
------------------------------------------------------------------
传入一个32位网络字节序地址。
inet_ntoa函数的执行结果放在静态内存中,是不可重入的。
2.2.7 inet_pton,转换字符串到网络地址
------------------------------------------------------------------
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
返回:1 成功,0 输入格式不对,-1 失败。
------------------------------------------------------------------
family表示地址族,strptr为源地址字符串,addrptr为转换后的网络地址。
若参数family不被支持,则出错,errno置为EAFNOSUPPORT。
2.2.8 inet_ntop,转换网络地址到字符串
------------------------------------------------------------------
#include <arpa/inet.h>
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
返回:指向结果的指针 成功;NULL 失败。
------------------------------------------------------------------
family表示地址族,addrptr是传入的网络地址,strptr存放转换后的结果,len防止缓存区溢出,如果缓存区大小不够,则返回NULL,并将errno置为ENOSPC。
若参数family不被支持,则出错,errno置为EAFNOSUPPORT。
2.2.9 readn、writen、readline函数
以上三个函数都在头文件"unp.h"中,下面是这三个函数的具体实现:
------------------------------------------------------------------
readn函数:从一个描述字读取n字节。[lib/readn.c]
ssize_t readn(int fd, void *vptr, size_t n) { ssize_t nleft = n; ssize_t nread = 0; char *ptr = vptr; while (nleft > 0) { if ((nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) nread = 0; //call read() again else return -1; } else if (nread == 0) { break; } nleft -= nread; ptr += nread; } return (n-nleft); }
------------------------------------------------------------------
------------------------------------------------------------------
write函数:往一个描述子写n个字节[lib/writen.c]
ssize_t writen(int fd, const void *vptr, size_t n) { ssize_t nleft = n; ssize_t nwritten = 0; const char *ptr = vptr; while (nleft > 0) { if ((nwritten = write(fd, ptr, nleft)) <= 0) { if (errno == EINTR) { nwritten = 0; //call write() again } else { return -1; } } nleft -= nwritten; ptr += nwritten; } return n; }
------------------------------------------------------------------
------------------------------------------------------------------
readline函数:从一个描述字读取一样文本,一次读一个字节。
ssize_t readline(int fd, void *vptr, size_t maxlen) { ssize_t n = 0; ssize_t rc = 0; char c; char *ptr = vptr; for (n = 1; n<maxlen; ++n) { again: if ((rc = read(fd, &c, 1)) == 1) { *ptr++ = c; if (c == ‘\n‘) break; } else if (rc == 0) { if (n == 1) return 0; //no data read else break; //some data was read } else { if (errno == EINTR) goto again; return -1; } } *ptr = 0; return n; }
------------------------------------------------------------------
注意:EINTR是linux函数的返回状态,在不同的函数中意义不同,如下:
write,表示由于信号中断,没有写成功数据。
read,表示由于信号中断,没读成功数据。
sem_wait,函数调用被信号处理函数中断。
recv,由于信号中断返回,没有任何数据可用。
2.2.10 isfdtype,测试描述符类型
------------------------------------------------------------------
#include <sys/stat.h>
int isfdtype(int fd, int fdtype);
返回:1 是指定类型;0 不是指定类型;-1 出错。
------------------------------------------------------------------
要测试是否为套接口描述子,fdtype应设为S_IFSOCK。
2.2.11 socket,用来获取一个文件描述符
------------------------------------------------------------------
#include <sys/socket.h>
int socket(int family, int type, int protocol);
返回:非负 成功;-1 失败。
------------------------------------------------------------------
family是协议族,有五种取值:
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_FOUTE 路由套接口
AF_KEY 秘钥套接口
一般前两种使用较多。
type指定套接口类型,有三种:
SOCK_STREAM字节流套接口
SOCK_DGRAM数据报套接口
SOCK_RAW原始套接口
protocol在type不是原始套接口的情况下一般设为0。(原始套接口之后详细介绍)
注意:不是所有的family和type组合都是有效的。
2.2.12 connect,连接
当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址。分两种情况:字节流套接口,connect使用三次握手建立一个连接;数据报套接口,connect仅指明远程端地址,不向它发送任何数据。
------------------------------------------------------------------
#include <sys.socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
返回:0 成功;-1 失败。
------------------------------------------------------------------
在调用connect之前不必非得调用bind函数。
如果是TCP,则connect激发TCP的三路握手过程,在阻塞情况下,只有在连接建立成功或出错时该函数才返回。(三次握手协议之后介绍)
出错情况:
没有收到SYN分节的响应,在规定时间内经过重发仍无效,则返回ETIMEDOUT;
如果对SYN分节的响应是RST,表示服务器在指定端口上没有相应的服务,返回ECONNREFUSED;
如果发出 SYN在中间路由器上引发一个目的地不可达ICMP错误,在规定时间内经过重发仍无效,则返回EHOSTUNREACH或ENETUNREACH错误。
注意:
如果connect失败,则套接口将不能再使用,必须关闭,不能对此套接口再调用函数connect。
2.2.13 bind,为套接口分配一个本地IP和协议端口
对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合。如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
------------------------------------------------------------------
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
返回:0 成功;-1 失败。
------------------------------------------------------------------
对于IPv4,通配地址是INADDR_ANY,其值一般为0。使用方法如下:
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
对于IPv6,方法如下:
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any; (系统分配变量in6addr_any并将其初始化为常值IN6ADDR_ANY_INIT。)
注意:
如果让内核选择临时端口,bind并不返回所选的端口值,要得到一个端口,必须使用getsockname函数;
bind失败的常见错误是EADDRINUSE(地址已使用)。
2.2.14 listen,监听
listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
------------------------------------------------------------------
#include <sys/socket.h>
int listen(int sockfd, int backlog);
返回:0 成功;-1 失败。
------------------------------------------------------------------
第一个参数是socket函数返回的套接口描述字;第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:已完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手未完成的连接,accept函数是从已连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。
参数backlog曾经规定为监听套接口上的未完成连接队列和已完成连接队列总和的最大值,但各个系统的定义方法都不尽相同;历史上常把backlog置为5,但对于繁忙的服务器是不够的;backlog的设置没有一个通用的方法,依情况而定,但不要设为0。
2.2.15 accept,接收
accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
------------------------------------------------------------------
#include <sys/socket.h>
int accept(int sockfd, const struct sockaddr *cliaddr, socklen_t *addrlen);
返回:非负描述符 成功;-1 出错。
------------------------------------------------------------------
第一个参数是socket函数返回的套接口描述字;第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为NULL。
2.2.16 read和write,文件读写
当服务器和客户端的连接建立起来后,就可以进行数据传输了,服务器和客户端用各自的套接字描述符进行读/写操作。因为套接字描述符也是一种文件描述符,所以可以用文件读/写函数write()和read()进行接收和发送操作。
------------------------------------------------------------------
read:从文件描述符读。
#include <unistd.h>
int read(int sockfd, char *buf, int len);
返回:非负 成功;-1 失败。
------------------------------------------------------------------
------------------------------------------------------------------
write:写入文件描述符。
#include <unistd.h>
int write(int sockfd, char *buf, int len);
返回:非负 成功;-1 失败。
------------------------------------------------------------------
2.2.17 send和recv,发送和接收
TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的参数。
------------------------------------------------------------------
send:发送数据。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, void *buf, size_t size, int flags);
返回:发送的字节数 成功;-1 失败。
------------------------------------------------------------------
------------------------------------------------------------------
recv:接收数据。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
返回:接收的字节数 成功;-1 失败。
------------------------------------------------------------------
注意:
以上两个函数中flags参数为传输控制标记。(还不甚明白)
这里并没有sendto和recvfrom接口的介绍,之后会做两组接口的比较,就不在这里写了。
2.2.18 close,关闭套接口
------------------------------------------------------------------
#include <unistd.h>
int close(int sockfd);
返回:0 成功;-1 失败。
------------------------------------------------------------------
TCP套接口的close缺省功能是将套接口做上“已关闭”标记,并立即返回到进程。这个套接口描述字不能再为进程使用,但TCP将试着发送已排队待发的任何数据,然后按正常的TCP连接终止序列进行操作。
close把描述字的访问计数减1,当访问计数仍大于0时,close并不会引发TCP的四分组连接终止序列。若确实要发一个FIN,可以用函数shutdown。
3、简单的服务端、客户端例程
3.1 服务端代码
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> int main(int argc, char *argv[]) { int sockListen, sockConn; struct sockaddr_in serverAddr; struct sockaddr_in clientAddr; int sin_size, port; char hello[] = "Hello, world!\n"; if (argc != 2) { printf("parameter num err!\n"); return 0; } if ((port = atoi(argv[1])) < 0) { printf("the parameter should be a integer!\n"); return 0; } printf("port = %d\n", port); if ((sockListen = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("create sockListen err!\n"); return 0; } bzero(&serverAddr, sizeof(struct sockaddr_in)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(port); if (bind(sockListen, (struct sockaddr *)(&serverAddr), sizeof(struct sockaddr_in)) == -1) { printf("bind err!\n"); return 0; } if (listen(sockListen, 5) == -1) { printf("listen err!\n"); return 0; } while (1) { if ((sockConn = accept(sockListen, NULL, NULL)) == -1) { printf("accept err!\n"); return 0; } printf("sockConn = %d\n", sockConn); if (write(sockConn, hello, strlen(hello)) == -1) { printf("write err!\n"); return 0; } close(sockConn); } close(sockListen); return 0; }
3.2 客户端代码
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> int main(int argc, char *argv[]) { int sockConn; int port; char hello[512]; struct sockaddr_in serverAddr; if (argc != 3) { printf("parameter num err!\n"); return 0; } if ((port = atoi(argv[2])) < 0) { printf("the parameter should be a integer!\n"); return 0; } if ((sockConn = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("create socket err!\n"); return 0; } bzero(&serverAddr, sizeof(struct sockaddr_in)); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(port); if (inet_aton(argv[1], &serverAddr.sin_addr) == 0) { printf("addr err!\n"); return 0; } if ((connect(sockConn, (struct sockaddr *)&serverAddr, sizeof(struct sockaddr_in))) == -1) { printf("connect err!\n"); return 0; } bzero(hello, sizeof(hello)); read(sockConn, hello, sizeof(hello)); printf("%s\n", hello); close(sockConn); return 0; }
3.3 运行截图
服务端截图:
客户端截图:
程序说明:本程序只是简单的描述了服务端和客户端的基本工作过程,演示一下api的使用方法,在编码规范上还存在很多不足,比如错误处理全都是简单的输出、许多地方直接使用数字等等。
4、参考资料
http://www.cnblogs.com/riky/archive/2006/11/24/570713.aspx
http://blog.csdn.net/xiong_yao/article/details/8364633
UNIX网络编程-基本API介绍(一)