本文主要讲述linux下的socket编程和select相关的内容
目录
一、socket简介
socket是进行套接字编程的相关操作,套接字是一种通信机制,我们可以把我们的请求通过套接字发送给远程的一个服务机进行处理,也可以通过套接字处理来自客户机的相关请求。
socket分为服务端(server)端套接字和客户端(client)套接字,无论是服务端套接字还是客户端套接字,我们创建一个套接字的语言都是如下所示的
1.1 创建套接字
int socket(int domain,int type,int protol)
该方法是创建一个套接字,并且返回套接字的唯一标识
参数domain指的是套接字的域,我理解的就是指套接字作用的网络类型或者说是套接字所使用的网络介质,我们常用的domain有两个,分别是AF_UNIX和AF_INET,其中AF_UNIX指的是UNIX下的文件系统套接字,而AF_INET指的是正常的网络套接字,正如我们所熟悉的TCP/IP协议族。
参数type标明数据传递方式,在所有的网络传递中,数据传递只有两种类型,数据报和流,这里也是这样,分为SOCK_STREAM和SOCK_DGRAM,在AF_INET域下,SOCK_STREAM默认是TCP实现的,而SOCK_DGRAM则默认采用UDP实现。
参数protol标识通信所采用的协议,一般情况下,根据域和数据传递的方式就可以标识协议,因此该值我们一般设为默认值0
这样通过上面的代码,我们就创建了一个套接字。
1.2 套接字地址
我们知道,一个客户机想要获取一个远程的服务,最基本的途径就是去请求地址+端口,那么所有这些信息都被存储在套接字地址中,对于不同的套接字域,我们拥有不同的套接字地址,下面我们主要介绍AF_INET域下的套接字地址类型sockaddr_in
struct sockaddr_in{
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
}
sin_family类型为套接字域类型,即为AF_INET
sin_port指的是端口号
sin_addr指的是IP地址,其中in_addr的结构如下所示
struct sin_addr{
unsigend long int addr;
}
但是我们在赋值ip的时候,一般不适用直接传递long int类型的值,设置一个AF_INET域类型,端口号为9734,ip为本机(127.0.0.1)的套接字的代码如下所示。
address.sin_family=AF_INET;
address.sin_addr.s_addr=inet_addr("127.0.0.1");
address.sin_port=htons(9734);
1.3 client端套接字操作
client端创建好套接字,设置好套接字地址之后,下一步需要做的就是连接到服务端,所使用的方法名为connect
int connect(int socket,const struct sockaddr* addr,size_t address_length)
参数socket指的是套接字的唯一标识,即创建套接字方法返回的int值
参数addr是套接字地址指针
参数address_length是套接字地址长度,我们一般用sizeof(address)方法得到
链接成功的话,方法返回0,否则返回1
在连接之后,我们就可以使用write和read方法读取或者写入数据了
1.4 server端套接字
server端套接字的处理过程比client端要稍微复杂一点,首先server端要命名这个套接字,或者,我的理解是注册为服务端套接字,然后就开始无休止的监听(listen),当监听到来自客户端的套接字请求之后,server端会创建一个新的套接字,然后去跟client去交互,那么主套接字继续监听
命名套接字
bind(int socket,const struct sockaddr* addr,size_t address_length)
命名的方法跟client端请求连接方法的参数类型和所代码的含义完全相同,这样我们就把这个套接字标记为了server端的套接字
监听
int listen(int socket,int backlog)
参数socket是服务端套接字的标识符
参数backlog是等待队列的最大值,如果等待的客户端超过了这个值,那么就会被丢弃
接受连接
int accept(int socket.struct sockaddr* addr,siez_t* address_length)
accept参数接受一个客户端的请求,然后创建一个新的套接字去处理这个请求,返回的就是新创建的套接字的标识符。
参数socket是server端套接字的标识
参数addr是client端套接字地址
参数address_length是client端套接字地址长度
创建连接之后,就可以开始对套接字的写入或者读取操作,但是需要注意的是在操作完成之后,服务端要调用close函数关闭为client端所创建的套接字
代码清单中的server1.c 和client1.c实现了一个最简单的套接字程序,clinet向server发送一个字符,client获取之后将字符+1返还给client
二、select系统调用
我们在上面介绍了socket编程,在大多数情况下,我们知道server同时要处理多个client,那么一种方式我们可以采用fork()的方法去创建子进程,但是我们之前在多线程中讲过,创建进程的代价相对比较大,因此很难完成相应的操作。这里,我们要介绍另外一种机制可以帮助完成这个目标,select系统调用。
什么是select系统调用,通俗点讲就是我们的程序可以等待多个客户端的输入或者说是其他的操作,如果有某个客户端有操作,那么我们的客户端会立刻捕捉到某个客户端的操作,然后把处理他,如果没有操作的话,我们的程序就会一直阻塞,这样避免了同时多个客户端请求,但是其中一个客户端停滞时间(可能是输入、发呆。。。)过长,造成其他的客户端必须排队等待的情况。
select操作的对象不是socket,而是一种类型是fd_set的数据类型,我们可以简单的把他理解为一个包含可能发生操作的文件符集合(标准一点是打开的文件操作符集合)。我们有四个宏来操作这个集合
void FD_ZERO(fd_set* fdset);
void FD_ADD(int fd,fd_set* fdset);
void FD_CLR(int fd,fd_set* fdset);
void FD_ISSET(int fd,fd_set* fdset);
我们可以把fd_set类比成c++ 中的vector类型来理解上述四个方法
FD_ZERO初始化这个集合,并且把值设置0
FD_ADD添加特定fd到集合中,相当于vector的push_back
FD_CLR在fd_set集合中删除特定的fd,相当于vector中的delete
FD_ISSET判断给定的fd在不在集合中,相当于vector中的exist(c++ vector貌似没有,不过可以用find实现。。。反正是一个必须操作),在返回1,不在返回0
上述四个操作其实相当于一个集合的增删查和重置。
下面我们看一下真正的select操作函数的原型
int select(int nfds,fd_set* readdfs,fd_set* writefds,fd_set* errorfds,struct timeval* timeout)
参数nfds指定需要测试的描述符数量,我们一般宏FD_SETSIZE指定
参数readdfs,writedfs,errordfs分别代表了三种操作集合(作用是当readdfs中描述符可读,writefds中有描述符可写,errordfs中有描述符发生错误,我们的select就光荣完成任务,返回了)
参数timeout设置最长等待时间,超过这个时间,select也会返回
OK,那么问题来了,我们怎么知道是哪个操作符可读、可写或者发生错误?
是这样的,select在返回之前,假设select发现某个操作符可以读,那么它会把其他的那些操作符全部清为0,这样我们就可以对我们检测的所有操作符做个循环,使用FD_ISSET方法判断是不是在集合中。所以这里有个小tips,我们在使用select时,传递的那三个fd_set类型一定是个无用的副本。。。
代码清单中的select.c改写了server1.c,实现的功能相同,但是我们使用了select实现。
代码
client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
int socketfd;
int len;
struct sockaddr_in address;
int result;
char ch='A';
/**
为客户端创建一个套接字
**/
socketfd=socket(AF_INET,SOCK_STREAM,0);
/**
为套接字命名
**/
address.sin_family=AF_INET;
address.sin_addr.s_addr=inet_addr("127.0.0.1");
address.sin_port=htons(9734);
len=sizeof(address);
/**
将我们的套接字连接到服务器的套接字
**/
result=connect(socketfd,(struct sockaddr*)&address,len);
if(result==-1){
printf("OOPS:CLIENT1\n");
exit(EXIT_FAILURE);
}
/**
通过socket传输数据
**/
write(socketfd,&ch,1);
read(socketfd,&ch,1);
printf("we get the char %c\n",ch);
close(socketfd);
exit(EXIT_SUCCESS);
}
server1.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
int server_socketfd,client_socketfd;
int server_len,client_len;
struct sockaddr_in server_address;
struct sockaddr_in clent_address;
/**
delete the old socket
**/
unlink("server_socket");
server_socketfd=socket(AF_INET,SOCK_STREAM,0);
/**
name the socket
**/
server_address.sin_family=AF_INET;
server_address.sin_addr.s_addr=htonl(INADDR_ANY);
server_address.sin_port=htons(9734);
server_len=sizeof(server_address);
bind(server_socketfd,(struct sockaddr*)&server_address,server_len);
/**
create a link queue, waiting for the clent
**/
listen(server_socketfd,5);
while(1){
char ch;
printf("server waiting\n");
client_len=sizeof(clent_address);
client_socketfd=accept(server_socketfd,(struct sockaddr*)&clent_address,&client_len);
read(client_socketfd,&ch,1);
ch++;
write(client_socketfd,&ch,1);
close(client_socketfd);
}
}
select.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
int main(){
int server_socketfd,client_socketfd;
int server_len,client_len;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int result;
fd_set readfds,testfds;
server_socketfd=socket(AF_INET,SOCK_STREAM,0);
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
server_addr.sin_port=htons(9734);
server_len=sizeof(server_addr);
bind(server_socketfd,(struct sockaddr*)&server_addr,server_len);
listen(server_socketfd,5);
FD_ZERO(&readfds);
FD_SET(server_socketfd,&readfds);
while(1){
char ch;
int fd;
int nread;
testfds=readfds;
printf("server waiting\n");
result=select(FD_SETSIZE,&testfds,(fd_set*)NULL,(fd_set*)NULL,(struct timeval*)0);
if(result<1){
printf("select runs wrong\n");
exit(1);
}
/**
use rountine to find out which fd_set has changed
**/
for(fd=0;fd<FD_SETSIZE;fd++){
if(FD_ISSET(fd,&testfds)){
if(fd==server_socketfd){
client_len=sizeof(client_addr);
client_socketfd=accept(server_socketfd,(struct sockaddr*)&client_addr,&client_len);
FD_SET(client_socketfd,&readfds);
printf("adding client on fd %d\n",client_socketfd);
}
else{/**theat means it's not the server,but the client changed**/
ioctl(fd,FIONREAD,&nread);
if(nread==0){
close(fd);
FD_CLR(fd,&readfds);
printf("removing client on %d\n",fd);
}
else{
read(fd,&ch,1);
sleep(5);
printf("servering on fd %d\n",fd);
ch++;
write(fd,&ch,1);
}
}
}
}
}
}