socket编程在TCP/IP协议中“IP地址+端口号”就称为socket。首先我们来看看socket API,即TCP/IP协议的应用层编程接口。
我们接下来编写一个简单的客户端/服务器端通信的简单的模型,要遵循以下的方法:
1.创建套接字,我们要用到的函数是socket,以下是它的基本信息
其中,domain参数表明我们要用的协议类型,我们用AF_INET(IPV4),type参数表明协议实现的方式,因为我们都知道,TCP是面向字节流的,因此type就是SOCK_STREAM;protocol我们默认为0就可以了;
2.填充socket信息,在这里我们用到一个结构体,如下:
我们要用上面的结构体把IP地址及端口号填充进去,我们后面就可以很方便的使用它了。接下来我们看看参数:
sin_port是端口号,在结构体里面还有个成员是sin_family,表明我们所使用的协议,我们这里使用ipv4,所以是AF_INET;sin_addr是ip地址,它也是个结构体,如下:
由于我们通常使用的是点分十进制字符串表示IP地址,以下函数可以在字符串表示和in_addr之间转换:
我们下面编程使用的是inet_addr这个函数,它的参数cp就是IP地址号,把IP地址转换为32位的无符号整数,相反,我们可以使用inet_ntoa这个函数把无符号整数转换成我们的ip地址。
在计算机内存中有大小端之分,网络数据流同样有大小端之分,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机也是把从网络上接收到的字节流从低到高的顺序保存,因此,网络数据流的地址这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议也规定网络字节序采用大端字节序,即高位数存在低地址。所以会出现这样一种情况,假如发送端和接收端的字节序不一样(即一个大端一个小端,这样是不是就会出问题呢?),所以我们需要考虑字节序的转换,为使网络程序具有可移植性,使同样的代码在大端机和小端机上都能运行,我们可以调用下面的函数做网络字节序和主机字节序的转换:
在上面的函数中,h代表我们主机,n代表网络,l代表32位整数,s代表16位短整数,我们在下面的程序中就要用到,比如用htonl函数把端口号进行相应的转换等。
3.绑定套接字:
我们在上面创建了了一个套接字,并把相应的网络信息填充到网络结构体中去了,我们接下来就要把他们进行绑定,用到的函数就是bind,如下:
bind函数为套接字sockfd指定本地地址my_addr,my_addr的长度为addrlen(字节),也即给一个套接字分配一个名字。
4.把上面创建的套接字设置为监听套接字
用到的函数是listen函数,定义如下:
我们使用上面创建的套接字,调用listen函数使其能够自动接收到来的连接并且为连接队列制定一个长度限制,之后就可以使用accept函数接收连接。其中参数backlog指定未完成连接队列的最大长度。
5.服务端接收函数accept,定义如下:
accept函数用于基于连接的套接字,它从未完成队列中取出第一连接请求,创建一个和参数S属性相同的连接套接字,并为这个套接字分配一个文件描述符,然后以这个文件描述符返回,新创建的文件描述符不再处于监听状态;
6.连接函数(一般用于发送端)connect,定义如下:
下面我们来看看具体的代码:
服务器端:
#include <iostream> #include <stdlib.h> #include <errno.h> #include <string> #include <string.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <pthread.h> using namespace std; const int g_backlog=5; void usage(string _port) //返回参数错误的信息 { cout<<"usage: "<<_port<<"[ip][port]"<<endl; } static int start(const string &ip,const int &_port) { int listen_sock=socket(AF_INET,SOCK_STREAM,0);//创建套接字,参数:ipv4协议,字节流套字 if(listen_sock<0) { cerr<<strerror(errno)<<endl; exit(1); } struct sockaddr_in local; //填充sock信息 local.sin_family=AF_INET; local.sin_port=htons(_port); local.sin_addr.s_addr=inet_addr(ip.c_str()); if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0) //把sock和填充的信息进行绑定 { cerr<<strerror(errno)<<endl; exit(2); } if(listen(listen_sock,g_backlog)<0) //把该套接字设置为监听套接字 { cerr<<strerror(errno)<<endl; exit(3); } return listen_sock; } void *thread_run(void *arg) { int sock =(int)arg; char buf[1024]; while(1) { memset(buf,'\0',sizeof(buf)); ssize_t _size=read(sock,buf,sizeof(buf)-1); if(_size>0) { buf[_size]='\0'; } else if(_size==0) { cout<<"client close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<"client# "<<buf<<endl; } close(sock); return NULL; } int main(int argc,char* argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } struct sockaddr_in client; socklen_t len=sizeof(client); string ip=argv[1]; int port=atoi(argv[2]); int listen_sock=start(ip,port); while(1) { int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len); if(new_fd<0) { cerr<<strerror(errno)<<endl; continue; } cout<<"get a connect..."<<" sock :"<<new_fd<<" ip:"<<inet_ntoa(client.sin_addr)<<" port:"<<ntohs(client.sin_port)<<endl; #ifdef _V1_ char buf[1024]; while(1) { string _client=inet_ntoa(client.sin_addr); ssize_t _size=read(new_fd,buf,sizeof(buf)-1); if(_size>0) { buf[_size]='\0'; } else if(_size==0) { //client close cout<<_client<<"close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<"client#:"<<buf<<endl; } #elif _V2_ cout <<"v2"<<endl; pid_t id=fork(); if(id==0) { string _client=inet_ntoa(client.sin_addr); close(listen_sock); char buf[1024]; while(1) { memset(buf,'\0',sizeof(buf)); ssize_t _size=read(new_fd,buf,sizeof(buf)-1); if(_size>0) { buf[_size]='\0'; } else if(_size==0) { cout<<_client<<"close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<_client<<"# "<<buf<<endl; } close(new_fd); exit(0); } else if(id>0) { close(new_fd); } else {} #elif _V3_ pthread_t tid; pthread_create(&tid,NULL,thread_run,(void*)new_fd); pthread_detach(tid); #else cout<<"default"<<endl; #endif } return 0; } 客户端程序: void usage(string _port) //同样检查参数输入错误 { cout<<_port<<"[remote ip][remote port]"<<endl; } int main(int argc,char* argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } string ip=argv[1]; int r_port=atoi(argv[2]); int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<-1) { cout<<strerror(errno)<<endl; exit(1); } struct sockaddr_in remote; remote.sin_family=AF_INET; remote.sin_port=htons(r_port); remote.sin_addr.s_addr=inet_addr(ip.c_str()); int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote)); if(ret<0) { cout<<strerror(errno)<<endl; } string msg; while(1) { cout<<"plenter enter: "; cin >> msg; write(sock,msg.c_str(),msg.size()); } return 0; }
从上面的代码中我们可以看到,我们有3个版本,我们下面会介绍每一版本的不同,我们先用其中的一个版本看一下结果(V1版):
从上图我们可以看到,客户端发给服务器段的数据被服务器端收到了,证明连接建立成功了,可以收发数据了。
接下来我们说说上面几个版本的差别:
版本一(_V1_):版本一我们可以发现,版本一的服务端只能处理一个请求,服务端在收到一个请求时,就去处理这个请求,不再处于监听状态了,因此当再有请求来时,就不会处理这个请求了,显然这是个单处理模式,日常生活中这是不符合实际的,因此我们就衍生出了版本二(_V2_);
版本二(_V2_):在版本二中我们可以看到,我们用fork()一个子进程,让子进程处理客户端的连接请求,而父进程继续处于监听状态,等待下一个请求,这样就可以处理多个连接请求了,我们运行版本二看下结果:
我们可以看到,有两个客户端连接上了并发送了一些数据(由于我们是在同一台机子上测试,所以ip地址相同,端口号不同),这样我们就实现了多请求处理。在上面的版本二中,我们可以看见父进程并没有等在子进程,这样子进程运行结束后就会变成僵尸进程,但如果父进程以阻塞方式等待,就会和版本一一样(一次只能处理一个请求),如果以非阻塞方式等待,子进程同样不会被回收。我们也可以这样处理:我们注册一个信号处理函数,因为我们都知道子进程退出后,会给父进程发送一个SIGCHLD信号,我们可用捕捉该信号的方法来处理这种情况,同样,我们也可以利用线程来处理,就是版本三(_V3_);
版本三(_V3_):在版本三中,我们创建一个线程,并把线程这之城分离的,这样线程在退出的时候它的资源就会被主动回收;
现在 我们再来看一种情况,就是先启动server端,然后客户端连接上,然后用Ctrl+c,使server端退出,再次运行server端,就会出现如下情况:
这时因为孙然server端应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口,那么我们如何如何解决上述问题,使server端口在2MSL时间内能再次进行监听来接受新的连接呢?我们可以使用setsockopt函数,定义如下:
参数level为SOL_SOCKET,optname设置为SO_REUSEADDR;
我们只需在server端的代码socket函数和bind函数之间加上如下代码就可以了:
添加完上述代码后,我们再次运行程序,结果如下:
我们可以看到,当我们运行server端程序,收到一个连接后,我们马航退出server,再次运行server端不会再出现“地址被占用”的提示了,好了,上述问题搞定!!
至此,完成一个简单的客户/服务器基于TCP的服务。