1.套接字的地址结构
结构struct sockaddr定义了一种通用的套接字地址, 它在头文件linux/socket 中定义: struct socketsddr{ unsigned short sa_family; //地址类型,AF_xxx char sa_data[14]; // 14字节的协议地址 }; 其中sa_family表示套接字的协议族类型,对应于TCP/IP协议的AF_INET,成员sa_data存储具体的协议地址,sa_data之所以被定义成14字节的,因为有的协议族使用较长的地址格式,一般编程中并不对该结构体进行操作,而是使用另一个与它等价的数据结构:sockaddr_in. 每种协议族都有自己的协议地址格式,TCP/IP地址的格式为结构体struct sockaddr_in, 它在头文netinet/in.h中定义.格式如下: struct sockaddr_in{ unsigned short sin_family; //地址类型 unsigned short int sin_port; //端口号 struct in_addr sin_addr; //IP地址 unsigned char sin_size[8]; //填充字节一般为0 }; struct in_addr的定义如下: struct in_addr{ unsigned long s_addr; }; 结构体sockaddr的长度为16 字节,结构体sockaddr_in的长度也为16字节 ,通常使用sockaddr_in设置地址,然后强制类型转换为sockaddr; 来看例子:
struct sockaddr_in sock; sock.sin_family=AF_INET; sock.sin_port=htons(80); //设置端口 sock.sin_addr.s.addr=inet_addr("222.102.3.165"); //设置地址 memset(sock.sin_zero,0,sizeof(sock.sin_zero)); //将数组sin_zero清零
其中memet(void *s,int c,size_t n); 它将s指向的内存区域的前n个字节赋值由参数c 指定的值; 其中清零的字节处理函数还有bzero(),bcopy(),memcpy()等; void bzero(void *s,int n); 函数的具体操作是将参数s指定的前n个字节清零,
2.创建套接字
socket函数用来创建一个套接字. 头文件:#include <sys/types.h> #include <sys/socket.h> int socket(int domain,int type,int protocol); 参数domain用于指定创建套接字的所使用的协议族,它们在头文件<linux/socket.h>中,常用的协议族如下:
- AF_UNIX:创建只在本机内的通信的套接字.
- AF_INET: 使用IPV4TCP/IP协议;
- AF_INET6:使用IPV6TCP/IP协议;
参数type指定套接字的类型:
- SOCK_STREAM:创建TCP流套接字;
- SOCK_DGRAM:创建UDP数据报套接字;
- SOCK_RAW:创建原始套接字.
- 参数protocol通常设置为0;当创建原始套接字时 ,系统无法唯一确定协议,此时就需要使用该参数指定所使用的协议.执行成功返回一个新创建的套接字描述符.若有错误返回-1;
- RAW SOCKET能够对较低层次的协议直接访问,网络监听技术很大程度上依赖于它。该文介绍了利用RAW SOCKET捕获网络底层数据包的步骤和方法,并开发了一个程序模型来进一步探讨了利用RAW SOCKET捕获数据包的方法。
int sock_fd; sock_fd=socket(AF_INET,SOCK_STREAM,0); if(sock_fd < 0) { perror("socket"); }
3.建立连接
函数connect用来在一个指定的套接字上创建一个连接 头文件:#include <sys/types.h> #include <sys/socket.h> 函数原型:int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen) 参数sockfd是一个由函数socket创建的套接字,如果套接字的类型是SOCK_STREAM,则connect函数用于向服务器发出连接请求,服务器的IP地址和端口号由参数serv_addr指定,如果套接字的类型是SOCK_DGRAM,则connect函数并不建立真正的连接,它只是告诉内核与该套接字进行通信的目的地址(由第二个参数指定);只有该目的地址发来的数据才会被socket接收.对于SOCK_DGRAM类型的套接字,调用connect函数的好处是不必在每次发送和接收数据时都指定目的地址. 通常一个面向连接的套接字(如TCP套接字)只能调用一次connect函数,而对于无连接的套接字(如UDP套接字)则可以多次调用connect函数以改变与目的地址的绑定,将参数serv_addr中的sa_famliy设置为AF_UNSPEC,可以取消绑定. 参数serv_addr是一个地址结构. addrlen为参数serv_addr的长度,执行成功返回0,错误-1;
struct sockaddr_in serv_addr; memset(&serv_addr,0,sizeof(struct sockaddr)); //将serv_addr的个个字段清0; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(80); //htons是字节顺序转换函数, //inet_aton函数将一个字符串转换成网络地址,并把该网络地址赋给第二个参数; //inet族函数将在11.2.10小节介绍; if(inet_aton("172.17.242.131",&serv_addr.sin_addr)<0) { perror("inet_aton"); exit(1); } //使用sock_fd套接字连接到由serv_addr指定的目的地址上,假定sock_fd已定义; if(connect(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in))<0) { perror("connect"); exit(1); }
注意:serv_addr强制类型转换为struct sockaddr类型:
4.绑定套接字
函数bind用来将一个套接字和某个端口绑定在一起; #include <sys/types.h> #include <sys/socket.h> int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen); socket函数 只是创建了一个套接字,这个套接字将在哪个端口上,程序并没有指定,前面提到,在客户机/服务器模型中,服务器端的IP地址和端口号一般是固定的,因此服务器端的程序中,使用bind函数将一个套接字和某个端口绑定在一起,该函数一般只有在服务器端的程序调用,参数my_addr指定了sockfd将绑定到本地的地址 ,可以将参数my_addr的sin_addr设置为INADDR_ANY,而不是某个确定的IP地址就可以绑定到任何网络接口,对于只有一个IP地址的计算机,INADDR_ANY对应的就是它的IP地址,对于多宿主主机(拥有多块网卡),INADDR_ANY表示本服务器程序将处理来自所有网络接口上相应端口的连接请求.执行成功返回0,当有错误发生时-1; 该函数的常见用法如下;
struct sockaddr_in serv_addr; memset(&serv_addr,0,sizeof(struct sockaddr_in)); serv_addr.sin_famliy = AF_INET; serv_addr.sin_port=htons(80); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in))<0) { perror("bind"); exit(1); }
5.在套接字上监听
#include <sys/socket.h> int listen(int s,int backlog); 由于函数socket创建的套接字是主动套接字,这种套接字可以用来主动请求连接到某个服务器(通过函数connect),但是做为服务器端的程序 ,通常在某个端口上监听等待来自客户端的连接请求,在服务器端,一般先调用socket 函数创建一个套接字,然后调用函数bind将该套接字绑定到某个端口上,接着再调用函数listen将该套接字转换为监听套接字,等待来自客户端的连接请求, 一般多个客户端连接到一个服务器,服务器向这些客户端提供某种服务,服务器端设置一个连接队列,记录已经建立的连接,参数backlog用法非常简单,但是理解这个TCP通信的协议的通信过程非常重要,TCP协议为每个套接字实际上维持两个队列,一个是未完成连接的队列,即未完成3次握手的连接,另一个队列是完成了3次握手,但是未被服务器调用accept()函数接收的连接,参数backlog实际上指的是完成连接队列的最大长度.执行成功返回0,失败返回-1; 注意: 函数listen 只是将套接字设置为倾听模式以等待连接请求,它并不能接收连接请求,真正接收客户端连接请求的是accept()函数.
#define LISTEN_NUM 12 //定义完成连接请求队列长度 if(listen(sock_fd,LISTEN_NUM) < 0) { perror("listen"); exit(1); }
6.接收连接 #include <sys/types.h> #include<sys/socket.h> int accept(int sockfd,struct socket *addr,socklen_t *addren); 参数sockfd是我们转换成功的倾听套接字描述符,参数addr保存发起连接请求的主机地址和端口,参数addren是addr所指向的结构体的大小;当函数成功执行时,返回3个结果,函数返回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信. newsockfd=accept(sockfd,struct socket *addr,socket_t *addren)); 将客户机的详细信息存放在地址结构addr中,值的注意的是这个返回的套接字描述符与我们转换的倾听套接字是不同的,在一段服务器程序中,可以始终只用一个倾 听套接字来接收多个客户机的连接请求;而如果我们要和客户机建立一个实际的连接的话,对每一个请求我们都需要调用accept()返回一个新的套接字。当 服务器处理完毕客户机的请求后,一定要将相应的套接字关闭;如果整个服务器程序将要结束,那么一定要将倾听套接字关闭。