1、socket编程
socket这个词能够表不少概念: 在TCP/IP协议中,“IP地址+TCP或UDP端口号”惟标识络通信中的个进程,“IP地址+端口号”就称为socket。
在TCP协议中,建链接的两个进程各有个socket来标识,那么这两个socket组成 的socket pair就惟标识个链接。 socket本有“插座”的意思,所以来描述络链接的 对关系。
TCP/IP协议最先在BSD UNIX上实现,为TCP/IP协议设计的应层编程接称为socket APIios
2、相关函数:编程
socket 函数数组
套接字是通讯端点的抽象,实现端对端之间的通讯。与应用程序要使用文件描述符访问文件同样,访问套接字须要套接字描述符。任何套接字编程都必须调用socket 函数得到套接字描述符,这样才能对套接字进行操做。如下是该函数的描述:服务器
1. /* 套接字 */ 网络
2. 并发
3. /* socket
4. * 函数功能:建立套接字描述符; tcp
5. * 返回值:若成功则返回套接字非负描述符,若出错返回-1; ide
6. * 函数原型: 函数
7. */
8. #include <sys/socket.h>
9.
10. int socket(int family, int type, int protocol);
11. /*
12. * 说明:
13. * socket相似与open对普通文件操做同样,都是返回描述符,后续的操做都是基于该描述符;
14. * family 表示套接字的通讯域,不一样的取值决定了socket的地址类型,其通常取值以下:
15. * (1)AF_INET IPv4因特网域
16. * (2)AF_INET6 IPv6因特网域
17. * (3)AF_UNIX Unix域
18. * (4)AF_ROUTE 路由套接字
19. * (5)AF_KEY 密钥套接字
20. * (6)AF_UNSPEC 未指定
21. *
22. * type肯定socket的类型,经常使用类型以下:
23. * (1)SOCK_STREAM 有序、可靠、双向的面向链接字节流套接字
24. * (2)SOCK_DGRAM 长度固定的、无链接的不可靠数据报套接字
25. * (3)SOCK_RAW 原始套接字
26. * (4)SOCK_SEQPACKET 长度固定、有序、可靠的面向链接的有序分组套接字
27. *
28. * protocol指定协议,经常使用取值以下:
29. * (1)0 选择type类型对应的默认协议
30. * (2)IPPROTO_TCP TCP传输协议
31. * (3)IPPROTO_UDP UDP传输协议
32. * (4)IPPROTO_SCTP SCTP传输协议
33. * (5)IPPROTO_TIPC TIPC传输协议
34. *
35. */
connect 函数
在处理面向链接的网络服务时,例如 TCP ,交换数据以前必须在请求的进程套接字和提供服务的进程套接字之间创建链接。TCP 客户端能够调用函数connect 来创建与 TCP 服务器端的一个链接。该函数的描述以下:
1. /*
2. * 函数功能:创建链接,即客户端使用该函数来创建与服务器的链接;
3. * 返回值:若成功则返回0,出错则返回-1;
4. * 函数原型:
5. */
6. #include <sys/socket.h>
7.
8. int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
9. /*
10. * 说明:
11. * sockfd是系统调用的套接字描述符,即由socket函数返回的套接字描述符;
12. * servaddr是目的套接字的地址,该套接字地址结构必须包含目的IP地址和目的端口号,即想与之通讯的服务器地址;
13. * addrlen是目的套接字地址的大小;
14. *
15. * 若是sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址,即内核会肯定源IP地址,并选择一个临时端口号做为源端口号;
16. */
TCP 客户端在调用函数 connect 前没必要非得调用 bind 函数,由于内核会肯定源 IP 地址,并选择一个临时端口做为源端口号。若 TCP 套接字调用connect 函数将创建 TCP 链接(执行三次握手),并且仅在链接创建成功或出错时才返回,其中出错返回可能有如下几种状况:
若 TCP 客户端没有收到 SYN 报文段的响应,则返回 ETIMEOUT 错误;
若客户端的 SYN 报文段的响应是 RST (表示复位),则代表该服务器主机在咱们指定的端口上没有进程在等待与之链接。只是一种硬错误,客户端一接收到 RST 就当即返回ECONNERFUSED 错误;
RST 是 TCP 在发生错误时发送的一种 TCP 报文段。产生 RST 的三个条件时:
目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器;
TCP 想取消一个已有链接;
TCP 接收到一个不存在的链接上的报文段;
若客户端发出的 SYN 在中某个路由器上引起一个目的地不可达的 ICMP 错误,这是一个软错误。客户端主机内核保存该消息,并在必定的时间间隔继续发送 SYN (即重发)。在某规定的时间后仍未收到响应,则把保存的消息(即 ICMP 错误)做为EHOSTUNREACH 或ENETUNREACH 错误返回给进行。
bind 函数
调用函数 socket 建立套接字描述符时,该套接字描述符是存储在它的协议族空间中,没有具体的地址,要使它与一个地址相关联,能够调用函数bind 使其与地址绑定。客户端的套接字关联的地址通常可由系统默认分配,所以不须要指定具体的地址。若要为服务器端套接字绑定地址,能够经过调用函数 bind 将套接字绑定到一个地址。下面是该函数的描述:
1. /* 套接字的基本操做 */
2.
3. /*
4. * 函数功能:将协议地址绑定到一个套接字;其中协议地址包含IP地址和端口号;
5. * 返回值:若成功则返回0,若出错则返回-1;
6. * 函数原型:
7. */
8. #include <sys/socket.h>
9. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
10. /*
11. * 说明:
12. * sockfd 为套接字描述符;
13. * addr是一个指向特定协议地址结构的指针;
14. * addrlen是地址结构的长度;
15. */
对于 TCP 协议,调用 bind 函数能够指定一个端口号,或指定一个 IP 地址,也能够二者都指定,还能够都不指定。若 TCP 客户端或服务器端不调用bind 函数绑定一个端口号,当调用connect 或 listen 函数时,内核会为相应的套接字选择一个临时端口号。通常 TCP 客户端使用内核为其选择一个临时的端口号,而服务器端经过调用bind 函数将端口号与相应的套接字绑定。进程能够把一个特定的 IP 地址捆绑到它的套接字上,可是这个 IP 地址必须属于其所在主机的网络接口之一。对于 TCP 客户端,这就为在套接字上发送的 IP 数据报指派了源 IP 地址。对于 TCP 服务器端,这就限定该套接字只接收那些目的地为这个 IP 地址的客户端链接。TCP 客户端通常不把 IP 地址捆绑到它的套接字上。当链接套接字时,内核将根据所用外出网络接口来选择源 IP 地址,而所用外出接口则取决于到达服务器端所需的路径。若 TCP 服务器端没有把 IP 地址捆绑到它的套接字上,内核就把客户端发送的 SYN 的目的 IP 地址做为服务器端的源 IP 地址。
在地址使用方面有下面一些限制:
在进程所运行的机器上,指定的地址必须有效,不能指定其余机器的地址;
地址必须和建立套接字时的地址族所支持的格式相匹配;
端口号必须不小于1024,除非该进程具备相应的特权(超级用户);
通常只有套接字端点可以与地址绑定,尽管有些协议容许多重绑定;
listen 函数
在编写服务器程序时须要使用监听函数 listen 。服务器进程不知道要与谁链接,所以,它不会主动地要求与某个进程链接,只是一直监听是否有其余客户进程与之链接,而后响应该链接请求,并对它作出处理, 一个服务进程能够同时处理多个客户进程的链接。listen 函数描述以下:
1. /*
2. * 函数功能:接收链接请求;
3. * 函数原型:
4. */
5. #include <sys/socket.h>
6.
7. int listen(int sockfd, int backlog);//若成功则返回0,若出错则返回-1;
8. /*
9. * sockfd是套接字描述符;
10. * backlog是该进程所要入队请求的最大请求数量;
11. */
listen 函数仅由 TCP 服务器调用,它有如下两种做用:
当 socket 函数建立一个套接字时,若它被假设为一个主动套接字,即它是一个将调用connect 发起链接的客户端套接字。listen 函数把一个未链接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的链接请求;
listen 函数的第二个参数规定内核应该为相应套接字排队的最大链接个数;
listen 函数通常应该在调用socket 和bind 这两个函数以后,并在调用 accept 函数以前调用。 内核为任何一个给定监听套接字维护两个队列:
未完成链接队列,每一个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于 SYN_REVD 状态;
已完成链接队列,每一个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于 ESTABLISHED 状态;
accept 函数
accept 函数由 TCP 服务器调用,用于从已完成链接队列队头返回下一个已完成链接。若是已完成链接队列为空,那么进程被投入睡眠。该函数的返回值是一个新的套接字描述符,返回 值是表示已链接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器一般仅仅建立一个监听套接字,它在该服务器的生命周期内一直存在。内核 为每一个由服务器进程接受的客户链接建立一个已链接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已链接套接字就会被关闭。该函数描述以下:
1. /* 函数功能:从已完成链接队列队头返回下一个已完成链接;若已完成链接队列为空,则进程进入睡眠;
2. * 函数原型:
3. */
4. int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出错返回-1;
5. /*
6. * 说明:
7. * 参数 cliaddr 和 addrlen 用来返回已链接的对端(客户端)的协议地址;
8. *
9. * 该函数返回套接字描述符,该描述符链接到调用connect函数的客户端;
10. * 这个新的套接字描述符和原始的套接字描述符sockfd具备相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个连接,
11. * 而是继续保持可用状态并接受其余链接请求;
12. * 若不关心客户端协议地址,可将cliaddr和addrlen参数设置为NULL,不然,在调用accept以前,应将参数cliaddr设为足够大的缓冲区来存放地址,
13. * 而且将addrlen设为指向表明这个缓冲区大小的整数指针;
14. * accept函数返回时,会在缓冲区填充客户端的地址并更新addrlen所指向的整数为该地址的实际大小;
15. *
16. * 若没有链接请求等待处理,accept会阻塞直到一个请求到来;
fork 和 exec 函数
1. /* 函数功能:建立子进程;
2. * 返回值:
3. * (1)在子进程中,返回0;
4. * (2)在父进程中,返回新建立子进程的进程ID;
5. * (3)若出错,则范回-1;
6. * 函数原型:
7. */
8. #include <unistd.h>
9. pid_t fork(void);
10. /* 说明:
11. * 该函数调用一次若成功则返回两个值:
12. * 在调用进程(即父进程)中,返回新建立进程(即子进程)的进程ID;
13. * 在子进程返回值是0;
14. * 所以,能够根据返回值判断进程是子进程仍是父进程;
15. */
16.
17. /* exec 序列函数 */
18.
19. /*
20. * 函数功能:把当前进程替换为一个新的进程,新进程与原进程ID相同;
21. * 返回值:若出错则返回-1,若成功则不返回;
22. * 函数原型:
23. */
24. #include <unistd.h>
25. int execl(const char *pathname, const char *arg, ...);
26. int execv(const char *pathnam, char *const argv[]);
27. int execle(const char *pathname, const char *arg, ... , char *const envp[]);
28. int execve(const char *pathnam, char *const argv[], char *const envp[]);
29. int execlp(const char *filename, const char *arg, ...);
30. int execvp(const char *filename, char *const argv[]);
31. /* 6 个函数的区别以下:
32. * (1)待执行的程序文件是 文件名 仍是由 路径名 指定;
33. * (2)新程序的参数是 一一列出 仍是由一个 指针数组 来引用;
34. * (3)把调用进程的环境传递给新程序 仍是 给新程序指定新的环境;
35. */
exec 6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较。
查找方式:前4个函数的查找方式都是完整的文件目录路径 pathname ,而最后两个函数(也就是以p结尾的两个函数)能够只给出文件名 filename,系统就会自动按照环境变量 “$PATH” 所指定的路径进行查找。
参数传递方式:exec 序列函数的参数传递有两种方式:一种是逐个列举的方式,而另外一种则是将全部参数总体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为 “l”(list)的表示逐个列举参数的方式,其语法为 const char *arg;字母为 “v”(vertor)的表示将全部参数总体构造指针数组传递,其语法为 char *const argv[]。读者能够观察 execl()、execle()、execlp() 的语法与 execv()、execve()、execvp() 的区别。这里的参数实际上就是用户在使用这个可执行文件时所需的所有命令选项字符串(包括该可执行程序命令自己)。要注意的是,这些参数必须以NULL结 束。
环境变量:exec 序列函数能够默认系统的环境变量,也能够传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle() 和 execve() 就能够在 envp[] 中指定当前进程所使用的环境变量。
1. 表 1 exec 序列函数的总结
2. 前4位 统一为:exec
3. 第5位 l:参数传递为逐个列举方式 execl、execle、execlp
4. v:参数传递为构造指针数组方式 execv、execve、execvp
5. 第6位 e:可传递新进程环境变量 execle、execve
6. p:可执行文件查找方式为文件名 execlp、execvp
并发服务器
当要求一个服务器同时为多个客户服务时,须要并发服务器。TCP 并发服务器,它们为每一个待处理的客户端链接调用 fork 函数派生一个子进程。当一个链接创建时,accept 返回,服务器接着调用 fork 函数,而后由子进程服务客户端,父进程则等待另外一个链接,此时,父进程必须关闭已链接套接字。
close 和 shutdown 函数
当要关闭套接字时,可以使用 close 和 shutdown 函数,其描述以下:
1. /* 函数功能:关闭套接字,如果在 TCP 协议中,并终止 TCP 链接;
2. * 返回值:若成功则返回0,若出错则返回-1;
3. * 函数原型:
4. */
5. #include <unistd.h>
6. int close(int sockfd);
7.
8. /*
9. * 函数功能:关闭套接字上的输入或输出;
10. * 返回值:若成功则返回0,若出错返回-1;
11. * 函数原型:
12. */
13. #include <sys/socket.h>
14. int shutdown(int sockfd, int how);
15. /*
16. * 说明:
17. * sockfd表示待操做的套接字描述符;
18. * how表示具体操做,取值以下:
19. * (1)SHUT_RD 关闭读端,即不能接收数据
20. * (2)SHUT_WR 关闭写端,即不能发送数据
21. * (3)SHUT_RDWR 关闭读、写端,即不能发送和接收数据
22. *
23. */
getsockname 和 getpeername 函数
为了获取已绑定到套接字的地址,咱们能够调用函数 getsockname 来实现:
1. /*
2. * 函数功能:获取已绑定到一个套接字的地址;
3. * 返回值:若成功则返回0,若出错则返回-1;
4. * 函数原型:
5. */
6. #include <sys/socket.h>
7.
8. int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);
9. /*
10. * 说明:
11. * 调用该函数以前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小;
12. * 返回时,该整数会被设置成返回地址的大小,若是该地址和提供的缓冲区长度不匹配,则将其截断而不报错;
13. */
14. /*
15. * 函数功能:获取套接字对方链接的地址;
16. * 返回值:若成功则返回0,若出错则返回-1;
17. * 函数原型:
18. */
19. #include <sys/socket.h>
20.
21. int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);
22. /*
23. * 说明:
24. * 该函数除了返回对方的地址以外,其余功能和getsockname同样;
25. */
3、相关代码及运行结果:
//tcp_server.cpp
#include<iostream> #include<stdlib.h> #include<sys/socket.h> #include<sys/types.h> #include<arpa/inet.h> #include<netinet/in.h> #include<errno.h> #include<string.h> #include<string> #include<pthread.h> using namespace std; const int g_backlog=5; void usage(string _proc) { cout<<"Usage:"<<_proc<<"[ip] [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); } //5 return 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)//read success { 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); } string ip=argv[1]; int port=atoi(argv[2]); int listen_sock=startup(ip,port); 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) { ssize_t _size=read(new_sock,buf,sizeof(buf)-1); if(_size>0)//read success { buf[_size]='\0'; } else if(_size==0)//client close {} else { cout<<strerror(errno)<<endl; } cout<<"client#"<<buf<<endl; } #elif _v2_ cout<<"v2"<<endl; pid_t id=fork(); if(id==0)//child { std::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_sock,buf,sizeof(buf)-1); if(_size>0)//read success { buf[_size]='\0'; } else if(_size==0) { cout<<_client<<"close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<_client<<"#"<<buf<<endl; } close(new_sock); exit(0); } else if(id>0)//father { close(new_sock); } else {} #elif _v3_ pthread_t tid; pthread_create(&tid,NULL,thread_run,(void*)new_sock); pthread_detach(tid); #else cout<<"default"<<endl; #endif } return 0; }
//tcp_client.cpp
#include<iostream> #include<string> #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> 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,(struct sockaddr*)&remote,sizeof(remote)); if(ret<0) { cout<<strerror(errno)<<endl; } string msg; while(1) { cout<<"Please Enter:"; cin>>msg; write(sock,msg.c_str(),msg.size()); } return 0; }
运行结果为: