一、socket编程
1.socket编程
socket这个词可以表示很多概念: 在TCP/IP协议中,“IP地址+TCP或UDP端号”唯一标识网络通讯中的一个进程,“IP地址+端口号”就称为socket。在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socket pair就唯一标识一个连接。 socket本义有“插座”的意思,因此用来描述网络连接的一对一关系。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应层编程接称为socket API。
2. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘件中的多字节数据相对于件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是地址。
TCP/IP协议规定,网络数据流应采大端字节序,即低地址字节。例如上节的UDP段格式, 地址0-1是16位的源端号,如果这个端号是1000(0x3e8),则地址0是0x03,地址1是0xe8, 也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,地址 存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,不是1000。因此,发 送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的, 接到16位的源端号也要做字节序的转换。如果主机是端字节序的,发送和接收都不需要做转 换。同理,32位的IP地址也要考虑络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在端和端计算机上编译后都能正常运,可以调 以下库函数做络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例 如:htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果 主机是网端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
3. socket地址的数据类型及相关函数
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6等。然而,各种网络协议的地址格式并不相同,如下图所示:
sockaddr数据结构
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、 128位IP地址和一些 控制字段。 UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各 种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现 都有长度字段,如Linux就没有),后16位表示地址类型。 IPv4、 IPv6和UNIX Domain Socket的地 址类型分别定义为常数AF_INET、 AF_INET6、 AF_UNIX。这样,只要取得某种sockaddr结构体的 首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的 内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例 如bind、 accept、 connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都 用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下。
在此只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址。但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换。
字符串转in_addr的函数:
4.TCP socket编程的一般流程与接口
(1)服务器端:
a.获取有效的IP地址与端口号(port)(服务器端需要约定好的端口号与IP,方便客户直接与该IP下的该端口建立连接)
b.将IP与port转为网络通用格式(用上面提到的函数)
c.声明监听文件描述符 (int listen_sock),将该文件描述符”注册“为
套接字文件(listen_sock=socket(AF_INET,SOCK_STREAM,0))
参数:
AF_INET:IPv4套接字类型(说明地址类型格式)
SOCK_STREAM:TCP协议类型(提供面向连接的稳定数据传输)
d.给listen_sock绑定相应的信息( IP,port),因为socket套接字是由内核接管处理的,因此我们无法直接操作,写入信息需要以下操作:
1>声明struct sockaddr_in 结构体,将对因信息赋值给结构体对应单元
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=ip;
2>调用bind()函数,将ip,port信息写入(即绑定)套接字的
e.将listen_sock设为监听状态
backlog:设置最大可监听数量(计算机的所有逻辑实现都基于存储与计算,监听意味着要维护相应的内存,而计算机内存与性能有限,因此其数量也是有限制的。)
f.新声明一个有效的socket文件描述符 client_sock,用accept()从监听队列中取出一个client_sock绑定。此时我们就可以通过该文件描述符发送和读取数据。(在此注意缓冲区中是否有数据来读,是否有空间来写)与普通文件操作相同,我们可以用read()与write()完成读写。
server端实例:
#include <iostream> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <string.h> #include <pthread.h> #include <signal.h> using namespace std; const int g_backlog=5; void usage(string _proc) { cout<<_proc<<" [remote ip] [remote port]"<<endl; } static int startup(const string &ip,const int &port) { //1.创建套接字 int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0){ cerr<<strerror(errno)<<endl; exit(1); } //2.注册套接字 struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(port); local.sin_addr.s_addr=inet_addr(ip.c_str()); //3.绑定套接字 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){ cerr<<strerror(errno)<<endl; exit(2); } //4.监听 if(listen(sock,g_backlog)<0){ cerr<<strerror(errno)<<endl; exit(3); } //结束返回 return sock; } void * thread_run(void* arg) { int sock=(int) arg; char buf[1024]; while(1){ ssize_t _size=read(sock,buf,sizeof(buf)-1); if(_size<0){ cout<<strerror(errno)<<endl; }else if(_size==0){ cout<<"client close..."<<endl; break; }else{ buf[_size]=0; } cout<<"client# "<<buf<<endl; } close(sock); return NULL; } int main(int argc,char* argv[]) { // if(argc!=3){ // usage(argv[0]); // exit(1); // } // string ip=argv[1]; // int port=atoi(argv[2]); int listen_sock=startup("192.168.1.112",8080); struct sockaddr_in client; socklen_t len=sizeof(client); while(1){ int new_sock=accept(listen_sock,(struct sockaddr *)&client,&len); if(new_sock<0){ cerr<<strerror(errno)<<endl; continue; } cout<<"get a connect..."<<" sock : "<<new_sock<<" ip :"\ <<inet_ntoa(client.sin_addr)<<"port : "<<ntohs(client.sin_port)<<endl; #ifdef _V1_ //version 1 char buf[1024]; while(1){ memset(buf,0,1024); ssize_t size=read(new_sock,buf,sizeof(buf)-1); if(size>0){ cout<<"client#"<<buf<<endl; } else if(size==0){ cout<<"client close!"<<endl; break; } else{ cout<<strerror(errno)<<endl; } } #elif _V2_ //多进程 version 2 pid_t id=fork(); signal(SIGCHLD,SIG_IGN); if(id<0){ exit(-1); }else if(id>0){ close(new_sock); }else{ char buf[1024]; while(1){ close(listen_sock); memset(buf,0,1024); ssize_t size=read(new_sock,buf,sizeof(buf)-1); if(size>0){ cout<<"client# "<<buf<<endl; } else if(size==0){ cout<<"client close!"<<endl; break; } else{ cout<<strerror(errno)<<endl; } } close(new_sock); exit(0); } #elif _V3_ //多线程 pthread_t tid; if( 0<pthread_create(&tid,NULL,thread_run,(void*)new_sock)){ cout<<"xian cheng cuo wu"<<endl; } pthread_detach(tid); #endif } return 0; }
注意:在C/C++文件中用宏定义控制参与预处理的文本 需要加-D宏
例如: g++ -o 目标 源.cpp -D_V1_
(2)客户端
大部分流程与服务端类似,有几点不同:
1.不需要bind()(因为客户端不需要被大家知道,方便建立连接)由操作系统随机给即可。
2.不需要accept()该函数是服务器中与请求方建立连接用的,client是主动发起方,因此不需要。
3.需要用connect()来主动向服务器发出连接请求。
client端实例:
#include <iostream> #include <stdio.h> #include <string.h> #include <string> #include <arpa/inet.h> #include <stdlib.h> #include <netinet/in.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> using namespace std; void usage(string proc) { cout<<proc<<" [remote ip] [remote port]"<<endl; } int main(int argc,char* argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } int r_port=atoi(argv[2]); string r_ip=argv[1]; 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(r_ip.c_str()); int ret=connect(sock,(sockaddr*)&remote,sizeof(remote)); if(ret<0){ cout<<strerror(errno)<<endl; } while(1){ cout<<"please enter: "; char s[1024]; gets(s); if(strcmp(s,"quit")==0){ write(sock,"\0",strlen(s)); close(sock); break; } write(sock,s,strlen(s)); // ssize_t ret=read(sock,s,strlen(s)); // s[ret]=0; // printf("server# %s\n",s); } return 0; }
注意:以上程序是客户向服务器 单向通信的,如需要双向,只需对建立好的连接套接字相互进行 读、写数据,并对此进行合适的控制即可。(因为TCP/IP是全双工的)
5.端口复用 setsockopt(...);
在linux socket网络编程中,大规模并发TCP或UDP连接时,经常会用到端口复用:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET,
SO_REUSEADDR, (const void *) &opt, sizeof(opt));
那么什么是端口复用呢,如何理解呢,可以解释成如下:
在A机上进行客户端网络编程,加入它所使用的本地端口号是1234,如果没有开启端口复用的话,它用本地端口1234去连接B机再用本地端口连接C机时就不可以,若开启端口复用的话在用本地端口1234访问B机的情况下还可以用本地端口1234访问C机。若本地编程bind的本地端口号时0表示由系统分配端口,并开启端口复用的话表示系统分配的该端口号开启了复用。
若是服务器程序中监听的端口,即使开启了复用,也不可以用该端口望外发起连接了。