Linux中的服务器设计一般分为循环服务器和并发服务器,按照所使用的协议可以分为Tcp(Transmission Control Protocol 传输控制协议)和Udp(User Datagram Protocol 用户数据报协议)。但是循环服务器在linux上用的不多,它在同一时刻只能响应一个客户端的请求,效率很低,所以一般都采用并发服务器,可以同时处理多个客户端的请求。
众所周知,使用tcp协议来传输数据,有可靠性、和安全性的保障,但是效率没有udp的高。事物都是二面性的,tcp通过它的3次握手和4次挥手确保了数据的稳定传输,但牺牲了它的效率。具体协议内容不在多说,这都可以在RFC文档里面找到。
下面就来用代码实现一下并发tcp服务器。在写之前我们需要了解IP地址、端口号(Port)和套接字(Socket)的关系,以及在计算机内是怎么存放的。在使用IP地址和Port时要将其转化成统一的格式,及大端字节序,这是为了避免不同类别主机之间在数据交换时由于字节序不同而导致的差错。在转换时我们一般按照下面这么做,其中argv[1]和argv[2]表示我输入的IP地址和端口号,server_addr是sockaddr_in结构体变量。
server_addr.sin_family = AF_INET; //地址族IPV4
server_addr.sin_port = htons(atoi(argv[2])); //端口号转换
server_addr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址转换
Socket是一个编程接口,也是一种特殊的文件描述符,由于TCP/IP协议是一种国际规范,所以被写入到了内核中,但socket不仅限于TCP/IP,它有三种类型,分别为流式套接字、数据报套接字、原始套接字,不同的类型,所走的协议不同。
先介绍下两个重要的数据类型:sockaddr和sockaddr_in,他们都用来保存socket的信息,但是为了方便我们一般使用sockaddr_in数据类型,然后通过对地址的强制转换变成sockaddr类型。
实现并发服务器可以用线程和进程来创建,他们有一点区别,在用线程创建时,有时候可能会由于客户端过多,而文件描述符只有一个空间,使得系统分配出错,导致创建的套接字被覆盖,因此需要用malloc函数动态分配地址存放套接字。这里我们用进程来实现。
流程如下:
服务器端:
1.创建流式套接字,socket()
2.将IP和PORT,转成网络字节序,函数分别为inet_addr(IP)和htons(atoi(PORT))
3.讲IP和PORT进行绑定,函数为bind()
4.设置套接字为监听模式函数为listen()
5.用accept函数提取客户端的请求。注意accept成功返回的是新的套接字,千万记住。
6.来一路请求则创建子进程与客户端交互,父进程则处理僵尸态子进程并关闭accept返回的文件描述符,避免浪费
客户端:
1.创建流式套接字,socket()
2.将IP和PORT,转成网络字节序,函数分别为inet_addr(IP)和htons(atoi(PORT)),这里写的是服务器的IP和PORT
3.客户端可以不用绑定,系统会自动分配一个端口号
4.通过connect函数发起连接请求
上面的一些函数在网络编程中会一直使用,务必要记住,不知道怎么使用可以查看MAN手册。
参数说明:domain是地址族,就是你使用的什么协议。
type是套接字类型
protocol是协议编号,一般设为0,表示让系统去找。
参数说明:sockfd:socket成功返回的套接字的文件描述符
addr: 是上面定义的结构体变量名,注意强转和取地址
addrlen:是结构体变量的长度,用sizeof可以求出
参数说明:sockfd:socket成功返回的套接字的文件描述符
backlog:监听队列的最大长度,一般设置为5
参数说明:sockfd:socket成功返回的套接字的文件描述符
addr: 是上面定义的结构体变量名,注意强转和取地址
addrlen:是结构体变量的长度,用sizeof可以求出 ,注意要取地址
参数说明:sockfd:socket成功返回的套接字的文件描述符
addr: 是上面定义的结构体变量名,注意强转和取地址
addrlen:是结构体变量的长度,用sizeof可以求出
这些函数使用并不困难,只需要对照传参就好,每个参数的说明在man手册里面都可以查到。
服务器端源代码:
1 #include <sys/socket.h>
2 #include <netinet/in.h>
3 #include <arpa/inet.h>
4 #include <stdio.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <signal.h>
8 #include <sys/wait.h>
9 #include <sys/types.h>
10 #include <stdlib.h>
11
12 void signal_handler(int signum) //非阻塞处理僵尸态子进程
13 {
14 waitpid(-1,NULL,WNOHANG);
15 return;
16 }
17
18 int do_client(int sockfd)
19 {
20 int n;
21 char buf[1024];
22
23 while(1)
24 {
25 n = recv(sockfd,buf,sizeof(buf) - 1,0);
26 buf[n] = '\0';
27 printf("Recv %d bytes : %s.\n",n,buf);
28 if(strncmp(buf,"quit",4) == 0)
29 break;
30 }
31
32 exit(EXIT_SUCCESS);
33 }
34
35 //./server ip port
36 int main(int argc, const char *argv[])
37 {
38 int n;
39 pid_t pid;
40 int listen_fd;
41 int connect_fd;
42 struct sockaddr_in peer_addr;
43 struct sockaddr_in server_addr;
44 int addr_len = sizeof(struct sockaddr);
45
46 if(argc < 3)
47 {
48 fprintf(stderr,"Usage : %s ip prot.\n",argv[0]);
49 exit(EXIT_FAILURE);
50 }
51
52 if(signal(SIGCHLD,signal_handler) == SIG_ERR)
53 {
54 perror("Fail to signal");
55 exit(EXIT_FAILURE);
56 }
57
58 listen_fd = socket(AF_INET,SOCK_STREAM,0);
59 if(listen_fd < 0){
60 perror("Fail to socket");
61 exit(EXIT_FAILURE);
62 }
63
64 bzero(&server_addr,sizeof(server_addr));
65 server_addr.sin_family = AF_INET;
66 server_addr.sin_port = htons(atoi(argv[2]));
67 server_addr.sin_addr.s_addr = inet_addr(argv[1]);
68 if(bind(listen_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
69 {
70 perror("Fail to bind");
71 exit(EXIT_FAILURE);
72 }
73
74 listen(listen_fd,5);
75 printf("Listen ...\n");
76
77 //并发服务器:每提取一个请求就创建一个子进程/子线程和客户端进行交互
78 while(1)
79 {
80 connect_fd = accept(listen_fd,(struct sockaddr *)&peer_addr,&addr_len);
81 if(connect_fd < 0){
82 perror("Fail to accept");
83 exit(EXIT_FAILURE);
84 }
85
86 printf("****************************\n");
87 printf("Port : %d.\n",ntohs(peer_addr.sin_port)); //打印连接的PORT,转成 主机字节序
88 printf("Ip : %s.\n",inet_ntoa(peer_addr.sin_addr));//同上面一样要转换
89 printf("****************************\n");
90
91 if((pid = fork()) < 0) //提取请求之后创进子进程
92 {
93 perror("Fail to fork");
94 exit(EXIT_FAILURE);
95 }
96
97 if(pid == 0) //子进程与客户端交互
98 {
99 do_client(connect_fd);
100 }
101
102 //避免文件描述符浪费
103 close(connect_fd);
104 }
105
106 return 0;
107 }
客户端源代码:
1 #include <sys/socket.h>
2 #include <netinet/in.h>
3 #include <arpa/inet.h>
4 #include <stdio.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <signal.h>
8 #include <sys/wait.h>
9 #include <sys/types.h>
10 #include <stdlib.h>
11
12 //./server ip port
13 int main(int argc, const char *argv[])
14 {
15 int n;
16 char buf[1024];
17 int sockfd;
18 struct sockaddr_in server_addr;
19
20 if(argc < 3)
21 {
22 fprintf(stderr,"Usage : %s ip prot.\n",argv[0]);
23 exit(EXIT_FAILURE);
24 }
25
26 sockfd = socket(AF_INET,SOCK_STREAM,0);
27 if(sockfd < 0){
28 perror("Fail to socket");
29 exit(EXIT_FAILURE);
30 }
31
32 bzero(&server_addr,sizeof(server_addr));
33 server_addr.sin_family = AF_INET;
34 server_addr.sin_port = htons(atoi(argv[2]));
35 server_addr.sin_addr.s_addr = inet_addr(argv[1]);
36 if(connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
37 {
38 perror("Fail to bind");
39 exit(EXIT_FAILURE);
40 }
41
42
43 while(1)
44 {
45 fgets(buf,sizeof(buf),stdin);
46 buf[strlen(buf) - 1] = '\0';
47
48 send(sockfd,buf,strlen(buf),0);
49 }
50
51 return 0;
52 }
实验效果如下:
想想:当我关掉了客户端后,服务器为什么没有阻塞,这种情况在UDP中会出现吗?
用2个客户端与服务器交互,客户端发送内容,服务器接受!那么一个简单的tcp并发服务器就完成了。这只是一个简单的小实验,重点在与要掌握一下网络的基本概念,与相关系统函数的调用。