网络基础 — 浅析网络套接字

来源:互联网 发布:减肥吃什么知乎 编辑:程序博客网 时间:2024/06/05 06:26

                                               

                                                网络套接字







套接字概念
                                                                                                                                                                                                                           
 
套接字编程,套接字这个词可以表示很多概念:在TCP/IP协议中,“IP地址 + TCP或UDP端口号唯一标识网络通讯中的一个进程,q其

中"IP地址+端口号" 就称为网络套接字. 后面我会同一使用socket表示网络套接字.

在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接,socket本

身就有插座的意思,因此用来描述网络连接的一对一关系.TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口

称为socket API.

socket地址的数据类型及其相关函数:
socket API是一层抽象的网络编程接口,适用于各种 底层网络协议,如IPV4,IPV6,以及后面说哦UNIX Domain Socket 然而,各
种网络协议的地址格式并不相同.

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位表示整个结构体的长度,后16位表示地址类型,IPV4,IPV6
和UNIX Domain Socket 的地址类型分别定义为常数AF_INET,AF_INET6,AF_UNXI.这样,只要取得某种sockaddr结构体的首地址,不
需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体的内容.因此,socket API可以接受各种类型的
sockaddr结构体指针做参数,例如bind,accept,connet的函数,这些函数的参数应该设计成void *类型以便接受各种类型的指
针,但是sock API的实现早于ANSI C标准化,那是还没有void* 类型,因此这些函数的参数都用 struct sockaddr*类型表示,在传
递参数之前要强制类型转换一下,例如:

bind(listen_fd,(struct sockaddr*)&servaddr,sizeof(servaddr));

网络套接字编程相关函数
————————————————————————————————————————————————————————

socket函数:

函数功能:创建套接字描述符;  
返回值:若成功则返回套接字非负描述符,若出错返回-1;  
函数原型:int socket(int family, int type, int protocol);   
参数说明:socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;  
family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:  
1)AF_INET         IPv4因特网域  
2)AF_INET6        IPv6因特网域  
3)AF_UNIX         Unix域  
4)AF_ROUTE        路由套接字  
5)AF_KEY          密钥套接字  
6)AF_UNSPEC       未指定  
type确定socket的类型,常用类型如下:  
1)SOCK_STREAM     有序、可靠、双向的面向连接字节流套接字  
2)SOCK_DGRAM      长度固定的、无连接的不可靠数据报套接字  
3)SOCK_RAW        原始套接字  
4)SOCK_SEQPACKET  长度固定、有序、可靠的面向连接的有序分组套接字  
protocol指定协议,常用取值如下: 
1)0               选择type类型对应的默认协议  
2)IPPROTO_TCP     TCP传输协议  
3)IPPROTO_UDP     UDP传输协议  
4)IPPROTO_SCTP    SCTP传输协议  
5)IPPROTO_TIPC    TIPC传输协议  

accept函数:

accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。该函数描述如下:
函数功能:从已完成连接队列队头返回下一个已完成连接;若已完成连接队列为空,则进程进入睡眠;  
函数原型:int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
返回值:若成功返回套接字描述符,出错返回-1;
参数说明: 
参数 cliaddr 和 addrlen 用来返回已连接的对端(客户端)的协议地址;  该函数返回套接字描述符,该描述符连接到调用connect函数的客户端;
套接字描述符sockfd具有相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个链接,而是继续保持可用状态并接受其他连接请求; 若不关心客户端协议地址,可将cliaddr和addrlen参数设置为NULL,否则,在调用accept之前,应将参数cliaddr设为足够大的缓冲区来存放地址,并且将addrlen设为指向代表这个缓冲区大小的整数指针; accept函数返回时,会在缓冲区填充客户端的地址并更新addrlen所指向的整数为该地址的实际大小;若没有连接请求等待处理,accept会阻塞直到一个请求到来;

bind函数:

调用函数 socket 创建套接字描述符时,该套接字描述符是存储在它的协议族空间中,没有具体的地址,要使它与一个地址相关联,可以调用函数bind 使其与地址绑定。客户端的套接字关联的地址一般可由系统默认分配,因此不需要指定具体的地址。若要为服务器端套接字绑定地址,可以通过调用函数 bind 将套接字绑定到一个地址。下面是该函数的描述:
函数功能:将协议地址绑定到一个套接字;其中协议地址包含IP地址和端口号;  
返回值:若成功则返回0,若出错则返回-1;  
函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
参数说明:
sockfd 为套接字描述符;
addr是一个指向特定协议地址结构的指针;
addrlen是地址结构的长度;  
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以两者都不指定。
1)服务器在启动时会捆绑众所周知的端口。如果一个TCP客户或服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。对于TCP客户端来说,让内核来选择临时端口是正常的,除非应用需要预留端口;然而对于服务器来说却极为罕见,因为服务器是通过他们众所周知的端口被大家认识的。
2)进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户,这就为该套接字上发送的IP数据报指派了源IP地址。对于TCP服务器,这就限定该套接字只接收那些目的地为这个IP地址的客户连接。TCP客户通常不把IP地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源IP地址,而所用外出接口则取决于到达服务器所需的路径。如果TCP服务器没有把IP地址捆绑到它的套接字上,内核就把客户端发送的SYN的目的IP地址作为服务器的源IP地址。
调用bind()函数时,它的IP地址和端口号可以两个都指定,也可以不指定,我们来看看各种情况下内核会怎么调用?

如果指定端口号为0,那么内核就在bind调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核等到套接字已连接或已在套接字上发出数据报时才选择一个本地IP地址。如果让内核来为套接字选择一个临时端口号,那么必须注意,函数bind并不返回所选择的值。实际上,由于bind函数的第二个参数有const限定词,它无法返回所选之值。为了得到内核所选择的这个临时端口值,必须调用函数getsockname()来返回协议地址.

listen函数:

在编写服务器程序时需要使用监听函数 listen 。服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。listen 函数描述如下:
函数功能:接收连接请求;  
函数原型:int listen(int sockfd, int backlog);//若成功则返回0,若出错则返回-1;
*参数说明:  
sockfd是套接字描述符;  
backlog是该进程所要入队请求的最大请求数量;
为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字两个队列:
1.未完成链接队列,每个这样的SYN分节对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
2.已完成链接队列,每个已完成TCP三路握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED状态。
监听套接字的两个队列:
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手。下图展示了用这两个队列建立连接时所交换的分组。

当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。这么做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久能在这些队列中找到可用空间。要是服务器TCP立即响应以一个RST,客户端的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户端无法区别响应SYN的RST究竟意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”.

connect函数:

在处理面向连接的网络服务时,例如 TCP ,交换数据之前必须在请求的进程套接字和提供服务的进程套接字之间建立连接。TCP 客户端可以调用函数connect 来建立与 TCP 服务器端的一个连接。该函数的描述如下
函数功能:建立连接,即客户端使用该函数来建立与服务器的连接;  
返回值:若成功则返回0,出错则返回-1;  
函数原型:  int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);   
 * 参数说明:  
sockfd是系统调用的套接字描述符,即由socket函数返回的套接字描述符;  
servaddr是目的套接字的地址,该套接字地址结构必须包含目的IP地址和目的端口号,即想与之通信的服务器地址;  
addrlen是目的套接字地址的大小;  
TCP 客户端在调用函数 connect 前不必非得调用 bind 函数,因为内核会确定源 IP 地址,并选择一个临时端口作为源端口号。若 TCP 套接字调用connect 函数将建立 TCP 连接(执行三次握手),而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况:若 TCP 客户端没有收到 SYN 报文段的响应,则返回 ETIMEOUT 错误;若客户端的 SYN 报文段的响应是 RST (表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接。只是一种硬错误,客户端一接收到 RST 就立即返回ECONNERFUSED 错误;
RST 是 TCP 在发生错误时发送的一种 TCP 报文段。产生 RST 的三个条件时:
1.目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器;
2.TCP 想取消一个已有连接;
3.TCP 接收到一个不存在的连接上的报文段;
若客户端发出的 SYN 在中某个路由器上引发一个目的地不可达的 ICMP 错误,这是一个软错误。客户端主机内核保存该消息,并在一定的时间间隔继续发送 SYN (即重发)。在某规定的时间后仍未收到响应,则把保存的消息(即 ICMP 错误)作为EHOSTUNREACH 或ENETUNREACH 错误返回给进行

最简单的网络套接字通信


通信过程:

服务器编写: server.c

*********************************************************************> File Name: server.c> Author: ma6174> Mail: ma6174@163.com > Created Time: Wed 26 Jul 2017 11:37:48 PM PDT ************************************************************************/#include<stdio.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#include<unistd.h>#include<stdlib.h>#include<arpa/inet.h>#include<netinet/in.h>#define _BACKLOG_ 10int getsock(int port){int sock = socket(AF_INET,SOCK_STREAM,0);if(sock < 0){perror("socket");exit(1);}printf("%d: socket create is ok\n",sock);struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sock,(struct sockaddr*)&server,sizeof(server)) < 0){perror("bind");close(sock);exit(2);}printf("bind is ok\n");if(listen(sock, _BACKLOG_) < 0){perror("listen");close(sock);return 3;}printf("listen is ok\n");return sock;}void use(const char* a){printf("#%s [port_server]\n",a);}int main(int argc,char* argv[]){printf("main start\n");if(argc < 2){use(argv[0]);exit(6);}int listen_sock = getsock(atoi(argv[1]));struct sockaddr_in client;socklen_t len = sizeof(client);printf("wait accept.....\n");while(1){int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);if(new_sock < 0){perror("accept");close(new_sock);exit(4);}printf("[%s] [%d]:accept is ok\n",inet_ntoa(client.sin_addr),ntohs(\client.sin_port));pid_t pid = fork();if(pid < 0){close(new_sock);printf("process creation failed\n");continue;}else if(pid == 0){close(listen_sock);if(fork() > 0){exit(2);}else{while(1){fflush(stdout);char buf[1024];ssize_t i = read(new_sock, buf, sizeof(buf));if( i > 0){printf("[%s] [%d]:client say#%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);}else if(i == 0){ close(new_sock);printf("[%s] [%d]:client goodbye\n",inet_ntoa(client.sin_addr),ntohs(\                client.sin_port));break;}else{perror("read");break;}printf("Enter to [%s][%d]#",inet_ntoa(client.sin_addr),ntohs(\            client.sin_port));fflush(stdout);fgets(buf , sizeof(buf), stdin);buf[strlen(buf) - 1] = '\0';write(new_sock,buf,strlen(buf));}}}else{close(new_sock);waitpid(pid , NULL,0);}}close(listen_sock);return 0;}


客户端编写: client.c

/*************************************************************************> File Name: client.c> Author: ma6174> Mail: ma6174@163.com > Created Time: Thu 27 Jul 2017 01:20:36 AM PDT ************************************************************************/#include<stdio.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#include<unistd.h>#include<stdlib.h>#include<arpa/inet.h>#include<netinet/in.h>void use(char* a){printf("#%s [port_server]\n",a);}int main(int argc,char* argv[]){printf("main start\n");if(argc < 3){use(argv[0]);return 3;}printf("use is ok\n");int sock = socket(AF_INET,SOCK_STREAM,0);if(sock < 0){perror("socket");return 1;}printf("create socket is ok\n");struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));server.sin_addr.s_addr = inet_addr((argv[1]));int conn = connect(sock,(struct sockaddr*)&server , sizeof(server));if(conn < 0){perror("connect");close(sock);return 2;}while(1){printf("please enter#");fflush(stdout);char buf[1024];fgets(buf,sizeof(buf),stdin);buf[strlen(buf) - 1] = '\0';write(sock,buf,sizeof(buf));char* str = "quit";if(strcmp(buf,str) == 0){break;}printf("server echo#");fflush(stdout);ssize_t r2 = read(sock , buf, sizeof(buf));if(r2 > 0){printf("%s\n",buf);}else{continue;}}close(sock);printf("client goodbye!\n");return 0;}


具体调用过程: