首先引入常用的几个头文件:
#include<errno.h> // location:/usr/include/asm-generic/errno.h #include<sys/types.h> // location: /usr/include/netinet/types.h #include<sys/socket.h> // location:usr/include/i386-linux-gnu/sys/socket.h #include<netinet/in.h> // location:/usr/include/netinet/in.h #include <arpa/inet.h> // location:/usr/include/arpa/inet.h #include <netdb.h> #include <unistd.h> // 只关注与网络相关的函数
其次引入常用的结构体:
//结构体1 <sys/socket.h> struct sockaddr { unsigned short sa_family; /* 地址家族, AF_xxx */ char sa_data[14]; /*14字节协议地址*/ }; //说明:这个结构体是存储套接字信息的最基本的结构体,但是直接使用它是很困难的。sa_family可以是任意类型,但一般是AF_INET
//结构体2 <netinet/in.h> struct sockaddr_in { short int sin_family; /* 通信类型 */ unsigned short int sin_port; /* 端口,网络字节序 */ struct in_addr sin_addr; /* Internet 地址 ,网络字节序*/ unsigned char sin_zero[8]; /* 为了与sockaddr结构的长度相同而被加入*/ }; struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* transport layer port # */ uint32_t sin6_flowinfo; /* IPv6 traffic class & flow info */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* set of interfaces for a scope */ }; // 注意:IPv6结构体 struct sockaddr_in6,同样可以与struct sockaddr互相转换
说明:sockaddr和sockaddr_in是并列结构,但用sockaddr_in可以轻松的处理一些基本元素。因此,所有以sockaddr指针为参数的接口都可以用sockaddr_in的指针通过显示转换进行替换。同时sockaddr_in在初始化时应该使用函数 bzero() 或 memset() 来全部置零。总结,除非遇到特殊情况,否则不需要定义sockaddr类型的变量。
//补充结构体:<netinet/in.h> struct in_addr { unsigned long s_addr; }; struct in6_addr { uint8_t s6_addr[16]; /* IPv6 address */ };
//结构体3 <netdb.h> struct hostent { char *h_name; //地址的正式名称 char **h_aliases; // 空字节-地址的预备名称的指针 int h_addrtype; //地址类型; 通常是AF_INET。 int h_length; // 地址的比特长度 char **h_addr_list; //网络字节顺序的ip地址列表 #define h_addr h_addr_list[0] //为了与老版本的兼容 }; //存储一台主机的信息
注意:要充分利用 errno.h获取错误信息(在<errno.h>中有介绍,在<stdio.h>有用法)。
一、转换 //<arpa/inet.h>
1、网络字节序和主机字节序相互转换
你能够转换两种类型: short (两个字节)和 long (四个字节)。
htons()--"Host to Network Short"
htonl()--"Host to Network Long"
ntohs()--"Network to Host Short"
ntohl()--"Network to Host Long"
对于以上四个函数,每一个函数的参数的类型和返回值的类型都是相同的,即unsigned short 或 unsigned long。且出现错误均返回-1.
2、点分十进制与数值之间的转换
(1)int inet_pton(int af, const char *src, void *dst);
这个函数转换网络地址字符串到网络地址,第一个参数af是地址族,第二个参数*src是来源地址,第三个参数* dst接收转换后的数据。
inet_pton 是inet_addr的扩展,支持的多地址族有下列:
- af = AF_INET,src为指向字符型的地址,即点分十进制地址,函数将该地址转换为in_addr的结构体,并复制在*dst中。
- af = AF_INET6,src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中。
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0;成功的话返回1
(2)const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
这个函数转换网络二进制结构转换到字符串类型的地址,参数的作用和inet_pton相同,只是多了一个参数socklen_t cnt,他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC。
inet_ntop函数成功的话返回字符串的首地址,错误返回NULL;
二、socket基本函数
1、int socket(int domain, int type, int protocol); //<sys/socket.h>
domain 应该设置成 "AF_INET"
type 告诉内核 是 SOCK_STREAM 类型还是 SOCK_DGRAM 类型
protocol 设置为 0
socket() 只是返回你以后在系统调用中可能用到的 socket 描述符,或 者在错误的时候返回-1。全局变量 errno 中将储存返回的错误值。
2、int bind(int sockfd, struct sockaddr *my_addr, int addrlen); //<sys/socket.h>
说明:一旦你有一个套接字,你可能要将套接字和机器上的一定的端口关联 起来。(如果你想用listen()来侦听一定端口的数据,这是必要一步。即作为服务器端)如果你只想用 connect(),那么这个步骤没有必要(即作为客户端)。
sockfd 是调用 socket 返回的文件描述符。my_addr 是指向数据结构 struct sockaddr 的指针,它保存你的地址(即端口和 IP 地址) 信息。 addrlen 设置为 sizeof(struct sockaddr)。 如果函数执行成功,返回值为0,否则为-1。
/******************************可以这么用**************************/ my_addr.sin_port = 0; /* 随机选择一个没有使用的端口 */ my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的IP地址 */ /********************但严格来说应该这么用**************************/ my_addr.sin_port = htons(0); /* 随机选择一个没有使用的端口 */ my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用自己的IP地址 */
3、int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); //<sys/socket.h>
说明:sockfd 是系统调用 socket() 返回的套接字文件描述符。serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr。addrlen 设置 为 sizeof(struct sockaddr)。错误返回-1
4、int listen(int sockfd, int backlog); //<sys/socket.h>
说明:sockfd 是调用 socket() 返回的套接字文件描述符。backlog 是在进入队列中允许的连接数目。什么意思呢? 进入的连接是在队列中一直等待直到你接受 (accept() 请看下面的文章)连接。它们的数目限制于队列的允许。 大多数系统的允许数目是20,你也可以设置为5到10。 发生错误的时候返回-1
listen只是把一个未连接的套接口转变为一个被动的套接口,以便接受外来的连接而已。所以listen用一次就够了,不用放在循环里边。
listen应该理解为把本地ip和端口设置为监听,而不是监听client连接。
accept()是监听client连接。
5、int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //<sys/socket.h>
说明:将从连接请求队列中获得连接信息,创建新的套接字,并返回该套接字的文件描述符。新创建的套接字用于服务器与客户机的通信,而原来的套接字仍然处于监听状态。
sockfd 是套接字描述符。参数addr所指的结构会被系统填入远程主机的地址数据,参数addrlen为scokaddr的结构长度。这里的addrlen所指向的值,是必须初始化的,而且要初始化为一个大于等于sizeof(struct sockaddr)的值,而accept函数在执行后,会将实际值赋给addrlen所指向的值,故如果期望值小于实际值,就无法获得客户端信息。
成功返回新套接字描述符;失败返回-1。
注意,在系统调用 send() 和 recv() 中你应该使用新的套接字描述符 new_fd。
6、send() and recv()函数(适用于流式数据传输)
(1)int send(int sockfd, const void *msg, int len, int flags); //<sys/socket.h>
sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。
send() 返回实际发送的数据的字节数--它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是 发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送余下的数据。但是这里也有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。它在错误的时候返回-1。
(2)int recv(int sockfd, void *buf, int len, unsigned int flags); //<sys/socket.h>
sockfd 是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为0。
如果sockfd 的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把sockfd 的接收缓冲中的数据复制到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把sockfd 的接收缓冲中的数据复制完。recv函数仅仅是复制数据,真正的接收数据是协议来完成的),recv函数返回其实际复制的字节数。如果recv在复制时出错,那么它返回SOCKET_ERROR(-1);如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
7、sendto() and recvfrom()函数(适用于数据报式数据传输)
既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之前需要目标地址。
(1)int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen); //<sys/socket.h>
除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。
(2)int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen); //<sys/socket.h>
from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是客户端机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。
(3)注意
如果你用 connect() 连接一个数据报套接字,你可以简单的调 用 send() 和 recv() 来满足你的要求。这个时候依然是数据报套接字,依然使用 UDP,系统套接字接口会为你自动加上目标和源的信息。
8、close()和shutdown()函数
(1)int close(int fd); //<unistd.h>
返回值:成功返回0,出错返回-1。它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企图都将返回错误信息。
(2)int shutdown(int sockfd, int how); //<sys/socket.h>
sockfd 是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一:
- 0 - 不允许接受
- 1 - 不允许发送
- 2 - 不允许发送和接受(和 close() 一样)
shutdown() 成功时返回 0,失败时返回 -1
9、int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); //<sys/socket.h>
函数 getpeername() 告诉你在连接的流式套接字上谁在另外一边,一般用于服务器端获取客户端的信息。前提显而易见,必须是在连接建立成功之后。
sockfd 是连接的流式套接字的描述符。addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的 信息。addrlen 是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)。 函数在错误的时候返回 -1
10、int getsockname(int sockfd, struct sockaddr *addr, int *addrlen); //<sys/socket.h>
函数 getsockname() 与getpeername()相反,它获取的是与套接字关联的本地协议地址。客户端可以在connect成功返回后,通过getsockname得到分配给此连接的本地IP地址和本地端口号;服务器也可以在bind成功后使用该函数,但没什么意义。
sockfd 是连接的流式套接字的描述符。addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,用它存储得到的本地信息。addrlen 是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)。 函数在错误的时候返回 -1
11、int gethostname(char *hostname, size_t size); //<unistd.h>
它返回你程序所运行的机器的主机名字。hostname 是一个字符数组指针,它将在函数返回时保存 主机名。size是hostname 数组的字节长度。 函数调用成功时返回 0,失败时返回 -1
这一系列的系统调用如下,不再详细介绍(注意set的时候可能会涉及权限问题):
- getdomainname 取域名
- setdomainname 设置域名
- gethostid 获取主机标识号
- sethostid 设置主机标识号
- gethostname 获取本主机名称
- sethostname 设置主机名称
12、struct hostent *gethostbyname(const char * hostname); //<netdb.h>
获取给定主机名的主机信息。
gethostbyname() 成功时返回一个指向结构体 hostent 的指针;否则是个空 (NULL) 指针,但是和以前不同,不设置errno,而是设置h_errno 。用过 herror得到错误信息,用法与perror完全相同。
13、int select (int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout); //<sys/select.h>
对多路同步I/O进行轮寻。
Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、accept或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
会用到的信息:
(1)struct timeval 结构体
struct timeval{ long tv_sec; // seconds long tv_usec; // microseconds }
(2) maxfdp1应为最大描述符的值加1
(3)返回值
>0:就绪描述字的数目
-1:出错
0 :超时
(4)readset writeset exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个条件不感兴趣,就可以把它设为NULL。如果三个指针都为NULL,我们就有了一个比sleep()函数更为精确的定时器(sleep()以秒为最小单位,这个以微妙为单位)。
(5)select() 让你可以同时监视多个套接字。如果你想知道的话,那么它就 会告诉你哪个套接字准备读,哪个又准备写,哪个套接字又发生了例外 (exception)。
(6)对于fd_set类型的数据,可以通过下面的四个宏对它们进行操作:
- FD_ZERO(fd_set *set) - 清除一个文件描述符集合
- FD_SET(int fd, fd_set *set) - 添加fd到集合
- FD_CLR(int fd, fd_set *set) - 从集合中移去fd
- FD_ISSET(int fd, fd_set *set) - 测试fd是否在集合中
(7)下面是一个select的应用,注意struct timval类型的time,它的位置很关键,不能仅在循环外赋值,否则它的时间会累加,必须每次循环赋一次值,使其每次都从0开始计时。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/time.h> #include <errno.h> #include <string.h> int main() { char buf[1024]; fd_set readset; struct timeval time; int state; while(1) { FD_ZERO(&readset); FD_SET(0,&readset); time.tv_sec=3; time.tv_usec=0; state=select(1,&readset,NULL,NULL,&time); if(state<0) fprintf(stderr,"error:%s\nerror code:%d\n",strerror(errno),errno); else if(state==0) fprintf(stdout,"time out!\n"); else if(FD_ISSET(0,&readset)) { fgets(buf,1024,stdin); fprintf(stdout,"get the input:%s",buf); } } return 0; }