一. 套接字(socket)
socket英文为插座的意思,也就是为用户提供了一个接入某个链路的接口。而在计算机网络中,一个IP地址标识唯一一台主机,而一个端口号标识着主机中唯一一个应用进程,因此“IP+端口号”就可以称之为socket。
两个主机的进程之间要通信,就可以各自建立一个socket,其实可以看做各自提供出来一个“插座”,然后通过连接上“插座”的两头也就是由这两个socket组成的socket pair就标识唯一一个连接,以此来表示网络连接中一对一的关系。
下面就要讨论如何使用为TCP/IP协议设计的应用层编程接口socket API。
二. socket API
创建socket
函数参数中,
domain表示底层通信所使用的协议,有很多选项,这里一般使用IPv4,因此选择AF_INET;
type表示协议实现的方式,因为TCP是基于字节流的流服务,因此选择SOCK_STREAM;
protocol表示socket基于的协议,这里可以设置为0,因为由前面两个选项已经可以知道是哪个协议了;
-------------------------------------------------------------------------------------------
2. 设置socket信息
socket既然是由IP及端口号组成的,那么就要包含关于网络地址的信息,用下面的socket地址结构来完成:
sockaddr_in用来表示IPv4的地址结构,而另一个sockaddr_in6则表示IPv6的地址结构,都包含了IP地址和端口号字段;
sin_是协议的地址类型;
sin_port是端口号,为无符号短整型16位;
sin_addr是IP地址,也同样是一个结构体:
in_addr_t是无符号整型32位;
其实在结构体中还有一个成员是sin_family,用来表明底层所使用的协议,这里是使用的是IPv6因此可以赋值为AF_INET;
但是这里要提出的是,我们常用的IP地址是点分十进制的表示方法,因此需要进行转换:
我们要将点分十进制的IP表示转换成32位的无符号整型,可以用上面的inet_addr函数,而反过来可以用inet_ntoa函数;
-------------------------------------------------------------------------------------------
3. 网络字节序
机器中数据的存储方式有大端和小端之分,所谓大端就是高位存在低地址,低位存在高地址,而小端就是高位存在高地址,低位存在低地址。那么,对于网络中传输的数据流同样有大小端之分,发送主机通常将要发送缓冲区中的数据按内存地址从低到高的顺序发出,而接收主机同样按照从低地址到高地址的顺序将数据放入缓冲区中,因此,对于网络数据流的发送:先发送的数据是低地址,后发送的数据是高地址;而TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
因此,如果两个通信的主机都是大端机就没有问题,而如果其中一方是小端机就会使数据出错,因此,在发送之前需要进行主机字节序和网络字节序的转换:
上面的函数都是用于字节序之间的转换的,h代表host主机,n代表net网络,l代表32位的长整数,s代表16位的短整数,比如在TCP报文中的端口号就需要进行转换,如果是大端机就不转换直接返回原值,小端机就会进行相应的转换。
-------------------------------------------------------------------------------------------
4. 绑定socket
创建好一个socket并且设置完毕socket网络地址信息之后,就需要将其进行绑定:
函数参数中,
sockfd就是创建出来的socket;
addr是设置好的socket网络地址结构体的指针,这里的sockaddr其实就是一个通用的结构体类型,相当于void;
addrlen则是结构体的长度;
函数成功返回0,失败返回-1;
bind的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
-------------------------------------------------------------------------------------------
4. 监听
对于一个服务器来说,需要一直保持一个监听listen的状态时刻监听网络中是否有连接请求,因此,可以用一个socket来一直保持在监听状态:
函数参数中,
sockfd是创建的一个socket;
backlog用于描述当有多个连接请求的时候等待队列中所能够允许的最大等待数量;
-------------------------------------------------------------------------------------------
5. 接收
用于服务器端,当有连接请求的时候,需要有一个socket用于处理请求:
函数参数中,
sockfd是创建的一个socket,这个socket是和listen用同一个socket,因为是送监听处得到请求连接;
addr是用于描述请求连接一方的网络地址信息结构体的指针;
addrlen是上述结构体的大小;
函数成功会返回有效的接收到的socket描述符,失败返回-1并置错误码;
-------------------------------------------------------------------------------------------
6. 连接
用于发送请求连接的一方:
函数参数中,
sockfd是连接方创建的一个socket文件描述符;
addr因为是连接请求方,所以是远端要接收连接请求一方的网络socket地址信息;
addrlen是上述网络地址信息结构体的大小;
函数成功返回0,失败返回-1并置错误码;
三. 栗子时间
上面介绍了一些进行通信需要的基本的socket API,接下来就可以创建出自己的一个通信机制,比如一个服务器端和一个客户连接端:
首先为服务器端:
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> #define _BACKLOG_ 3 //监听状态中允许等待队列中请求的最大数 void usage(const char *argv)//传参判断错误输出 { printf("%s [ip] [port]\n", argv); exit(1); } int creat_listen_socket(int ip, int port)//创建监听socket { int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0) { perror("socket"); exit(2); } struct sockaddr_in server;//创建本地server网络地址信息 server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = ip; if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定 { perror("bind"); exit(3); } if(listen(sock, _BACKLOG_) < 0)//设置为监听状态 { perror("listen"); exit(4); } return sock; } int main(int argc, char *argv[]) { if(argc != 3)//当参数不为3的时候说明传参有误 usage(argv[0]); int ip = inet_addr(argv[1]);//获取IP int port = atoi(argv[2]);//获取端口号 int listen_sock = creat_listen_socket(ip, port);//获取监听socket struct sockaddr_in client;//创建client网络地址结构体,用于存放 socklen_t client_len = sizeof(client); while(1) { //处理连接请求,在accept阶段就是处理TCP三次握手连接的过程 int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len); char *client_ip = inet_ntoa(client.sin_addr); int client_port = ntohs(client.sin_port); if(accept_sock < 0) { perror("accept"); continue; } printf("client ip: %s port: %d\n", client_ip, client_port); //将客户端的信息进行输出 char *buf[1024]; while(1) { memset(buf, ‘\0‘, sizeof(buf)); size_t size = read(accept_sock, buf, sizeof(buf)-1); if(size < 0) { perror("read"); break; } else if(size == 0) { printf("client %s is out...\n", client_ip); exit(5); } else printf("client# %s\n", buf); } } return 0; }
下面是客户端设计:
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> void usage(const char* argv)//同样进行参数错误判断输出 { printf("%s [ip] [port]\n", argv); exit(1); } int main(int argc, char *argv[]) { if(argc != 3) usage(argv[0]); int server_ip = inet_addr(argv[1]); int server_port = atoi(argv[2]); int client_sock = socket(AF_INET, SOCK_STREAM, 0); //创建客户端socket if(client_sock < 0) { perror("socket"); exit(2); } //只是这里需要填写要接收连接请求一方的网络地址信息,也就是远端server的地址信息 struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = server_ip; //进行连接,发送连接请求 if(connect(client_sock, (struct sockaddr*)&server, sizeof(server)) < 0) { perror("connect"); exit(3); } char buf[1024]; while(1) { memset(buf, ‘\0‘, sizeof(buf)); gets(buf); size_t size = write(client_sock, buf, sizeof(buf)); if(size < 0) { perror("write"); continue; } } return 0; }
如上步骤可概括为:
server端:
创建listen监听套接字;
填写本地网络地址信息;
将监听套接字与网络地址信息进行绑定bind;
设置为listen状态监听网络中的连接请求;
accept接收远端client发送的连接请求,是TCP中建立连接的三次握手过程;
连接建立完毕进行数据接收;
client端:
创建本地套接字client_socket;
因为是发送连接请求的一方,所以并不需要监听,也并不需要绑定本地网络地址信息,注意并不是不可以绑定,而是没有必要,因为在发送请求时系统已经默认分配了一个端口号且将IP地址和端口号一并发送了过去;
进行连接请求connect;
连接建立完毕进行数据发送;
用两个终端分别运行server和client端:
左边是server端,右边是client端,本地IP地址是10.71.5.89,自定义端口号为1234;
首先运行server,server就会一直处于监听状态,当在client端输入了要连接的socket信息之后,会将连接请求发送过去,则server端处理连接请求,也就是连接成功会输出client端的socket信息;
之后连接成功就可以传输数据了;
当client结束的时候,也就是在TCP中的四次挥手过程,server端读取不到信息知道client端关闭,因此也就结束进程;至此,整个TCP传输过程就结束了;
但是在上面创建出来的server端可以发现,处于监听状态时当有连接请求就会进行处理,但是这个服务器也就只能处理一个连接了,在连接成功之后就会进行数据的传输,因此也就不会再监听了,也就是说当再有连接请求的时候就无法监听到自然也就处理不了了;所以上面的设计其实是不符合实际情况的,实际情况就是一个服务器可以处理多个连接请求;
将上面的程序进行改进的话,可以将监听部分和处理连接的部分分开,也就是当监听到有连接请求的时候就转交给accept去处理,自己继续进行监听,因此可以在每次监听到连接请求的时候就fork出一个子进程,让子进程处理连接的过程,这样的话就可以同时处理多个连接请求进行多方的数据传输;
程序设计如下:
//在上面server的程序中,当accept进行连接之后,如果连接成功,就可运行如下程序 pid_t id = fork();//首先创建出一个子进程 if(id < 0) perror("fork"); else if(id == 0) { close(listen_sock);//子进程中并不需要监听链路,因此可将相应的文件描述符关闭 char *buf[1024]; while(1)//开始进行处理数据的传输 { memset(buf, ‘\0‘, sizeof(buf)); size_t size = read(accept_sock, buf, sizeof(buf)-1); if(size < 0) { perror("read"); break; } else if(size == 0) { printf("client %s is out...\n", client_ip); exit(6); } else printf("client %s # %s\n", client_ip, buf); } } else//父进程只需要进行监听网络中的连接请求,并不需要进行连接处理 close(accept_sock);
运行程序:
因为是在一台主机上的不同终端下运行的,因此是同一个IP地址不同的端口号;
这样就可以实现一台服务器多个客户端的数据传输方式了;
但是,在上面改进的程序中还是存在问题,可以注意到父进程并没有等待子进程,也就是子进程运行结束后会变成僵尸进程;而如果父进程等待的方式是阻塞式等待,那么也就变成了第一种情况,并没有什么改进;而如果用的是非阻塞的方式等待,那么如果有一个子进程运行结束了,但是因为并没有连接父进程被阻塞在了accept处,那么同样子进程也不会被回收;
当子进程退出的时候,父进程会收到一个SIGCHLD的信号,可以用捕捉该信号的方法来处理,也就是注册一个处理SIGCHLD的函数来回收子进程;但是同样,除了上面的用多进程的方式处理,也可以用多线程的方式来进行listen和accept的分离处理;
程序设计如下:
pthread_t tid;//创建出一个线程 pthread_create(&tid, NULL, accept_fun, (void *)accept_sock); pthread_detach(tid);//将线程设置成分离状态,结束后不必等主线程回收资源
void* accept_fun(void *sock) { int accept_sock = (int)sock; char *buf[1024]; while(1) { memset(buf, ‘\0‘, sizeof(buf)); size_t size = read(accept_sock, buf, sizeof(buf)-1); if(size < 0) { perror("read"); break; } else if(size == 0) { printf("client is out...\n"); break; } else printf("client# %s\n", buf); } }
上面程序就可以解决子进程称为僵尸进程的问题;
当server端进行accept的时候,就是在进行TCP的三次握手,而进行传输完毕就要进行四次挥手断开连接的过程,我们知道首先发出断开连接请求的一方会进入一个状态是TIME_WAIT,而在TIME_WAIT状态里会等待2MLS时间,这里前面一篇博客分析过是4分钟,但是是可以设定的;
也就是如果server首先断开了连接,就会进入TIME_WAIT状态也就是会等待上2MSL时间才会真正的释放连接进入CLOSE状态再次等待新一轮的连接;而如果在TIME_WAIT状态中还没有过2MSL时间就想再次运行起server的话,就会出现如下情形:
如上server首先结束了进程也就是发送了断开连接的请求,而之后如果想再次运行server端进行监听的话会被告知地址正在被使用中,也就是上一次的连接还未完全断开;
那么如何解决上述的问题使server端避免进入2MSL时间在结束之后就能尽快的再次进行全网的监听呢?可以使用setsockopt函数,设置socket描述符的选项SO_REUSEADDR为1,其作用是可以允许重用本地端口号和地址:
函数参数中,
sockfd是创建出的socket文件描述符;
level被指定为SOL_SOCKET;
optname是对相应协议模块的描述,也就是设置sockfd描述符的选项,这里就设置为SO_REUSEADDR,允许重用本地地址和端口;
optval和optlen用于访问选项值,也就是optname,这里的值应设置为1;
函数成功返回0,失败返回-1并置相应的错误码;
在创建监听socket和绑定函数bind之间插入函数setsockopt:
int set = 1; if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &set, sizeof(set)) < 0) { perror("setsockopt"); exit(0); }
运行程序:
这样一来,在调用setsockopt函数之后,表示本地的地址和端口号可以被重用,就可以避免在server主动断开连接时候进入一个2MSL的等待时间而不能继续监听了;
《完》
本文出自 “敲完代码好睡觉zzz” 博客,请务必保留此出处http://2627lounuo.blog.51cto.com/10696599/1775559
原文:http://2627lounuo.blog.51cto.com/10696599/1775559