那一天,人们终于回想起了被BUG支配的恐惧
Toggle navigation
Home
AboutMe
Links
Archives
Tags
C语言编写服务端入门
2017-05-24 01:27:18
1106
0
0
weibo-007
# C语言编写服务端入门 ## 前言 本文使用C语言编写一个服务器,旨在更好的理解服务端/客户端程序,迭代服务器,并发服务器。这篇文章的例子很简单,就是当客户端连接上服务端之后,服务端给出一个“Hello World”回应。 ## C/S结构流程图 整个客户端,服务端交互流程可以用下图表示,服务端是优先启动进程并监听某一个端口,并且进程一直阻塞,直到有客户端连接进来,才开始处理客户端连接。 ![image](http://note.youdao.com/yws/api/personal/file/WEBa63201810546dae9722399b5f0fbfa60?method=download&shareKey=a98b3d18f1ae1f8975e1be87ea609b6a) ## 服务端 通过流程图可以看出,服务端涉及的Socket函数有socket, bind, listen, accept, read, write, close。使用这7个函数就可以编写出一个简易服务器。 ### socket函数 为了执行网络I/O,一个进程必须做的第一件事情就是创建一个socket函数,函数原型 ``` # family 表示协议族 # type 表示套接字类型 # protocol 表示传输协议 # 若成功返回非负描述符,若出错返回-1 int socket(int family, int type, int protocol); ``` 这个函数需要传入协议族,套接字类型,传输层协议三个参数。 协议族可以有以下取值 |family|说明| |--|--| |AF_INET|IPv4协议| |AF_INET6|IPv6协议| |AF_LOCAL|Unix域协议| |AF_ROUTE|路由套接字| |AF_KEY|密钥套接字| 套接字类型可以有以下取值 |type|说明| |--|--| |SOCK_STREAM|字节流套接字| |SOCK_DGRAM|数据报套接字| |SOCK_SEQPACKET|有序分组套接字| |SOCK_ROW|原始套接字| 传输层协议可以有以下取值 |protocol|说明| |--|--| |IPPROTO_TCP|TCP传输协议| |IPPROTO_UDP|UDP传输协议| |IPPROTO_SCTP|SCTP传输协议| 这里我们选择IPv4协议,使用字节流套接字,传输层选择TCP协议,所以第一段代码: ``` #include <stdio.h> #include <sys/socket.h> int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(server_sockfd == -1){ printf("socket error"); return -1; } } ``` ### bind函数 bind函数把一个本地协议地址赋予一个套接字,对于网际协议,协议地址就是IP加端口的组合,函数原型 ``` # sockfd 初始化的套接字 # myaddr 协议地址 # addrlen 协议地址长度 # 若成功返回0 出错返回-1 int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen) ``` 注意,这个函数不是必须的,如果不使用这个函数绑定一个特定的端口,那么内核会帮我们的套接字选择一个临时端口。作为服务器,一般不会这么做,需要指定特定的端口。 这个函数的第二个参数是协议地址,注意,这个协议地址已经有定义好的结构体,使用IPv4套接字结构地址时候,地址结构体定义如下 ``` struct sockaddr_in { uint8_t sin_len; /*结构体长度*/ sa_family_t sin_family; /*AF_INET*/ in_port_t sin_port; /*端口(16-bie)*/ struct in_addr sin_addr; /*IPv4地址(32-bit)*/ char sin_zero[8]; /*没啥用,设置0即可*/ } ``` 我们让我们的服务器绑定8887端口(80端口被web占用了,用8887端口代替),所以我们的第二段代码 ``` #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); return -1; } } ``` ### listen函数 listen函数仅有服务器调用,它完成两件事情: 1. 当使用socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将发送connect发起连接的客户端套接字。当调用listen函数之后,它被转成一个被动套接字,只是内核应该接受连接请求。所以,调用listen之后套接字由CLOSED状态转到LISTEN状态 2. 这个函数规定内核应该为相应套接字排队的最大连接数 函数原型 ``` /*失败时返回-1*/ int listen(int sockfd, int backlog) ``` backlog参数的设定其实是表示两个队列的总和,这两个队列分别是 1. 未完成连接队列,在客户端发送一个SYN直到三次握手完成,都是这个状态,SYN_RCVD状态。 2. 已完成连接队列,这个表示三次握手完成的状态,ESTABLISHED状态 因为我们是测试,这个值设置成20就可以了。所以我们的第三段代码 ``` #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); return -1; } if(listen(server_sockfd, 20) == -1){ printf("listen error"); return -1; } } ``` ### accept函数 accept函数是由TCP服务器调用,用于从已完成连接队列的队头返回下一个已完成连接,如果已完成连接队列为空,那么进程进入睡眠模式,函数原型 ``` # sockdf 服务器套接字莫描述符 # cliaddr 已连接的客户端协议地址 # addrlen 已连接的客户端协议地址长度 # 成功返回非负描述符,出错返回-1 int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); ``` 当accept成功时,返回值是由内核自动生成的全新描述符,代表与所返回的客户端TCP连接。所以,在我们讨论accept函数时,我们称第一个参数为**监听套接字**,它的返回值是**已连接套接字**,一个服务器通常指创建一个监听套接字(通常是80端口),内核为每个由服务器进程接受的客户端连接创建一个已连接套接字,当服务器完成对某个给定的客户端服务时,连接就会被关闭。 函数的第二个参数也是一个协议地址结构体,这个结构体和服务端协议地址是同一个结构体。我们可以不关心客户端的协议,直接传空,我们关系的是这个函数的返回值,因为它返回的是客户端连接描述符,我们可以对这个描述符进行写操作,从而实现给客户端传输数据。所以我们第四段代码 ``` #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); exit(1); } if(listen(server_sockfd, 20) == -1){ printf("listen error"); exit(1); } struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ printf("appect error"); return -1; } } ``` ### write函数 前面的操作都完成之后,说明服务端和客户端已经建立连接,由于TCP的传输是全双工的,这时候客户端和服务端都可以向对方发送数据。这里为了简化,我们实现服务端发送“Hello World”给请求连接的客户端。给客户端发送数据很简单,就是对返回的客户端描述符进行写操作就可以了 ``` # sockfd socket文件描述符 # buf 文件内容 # count 内容长度 ssize_t write(int sockfd, const void * buf, size_t count); ``` ### 完整的服务器代码 ``` #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); return -1; } if(listen(server_sockfd, 20) == -1){ printf("listen error"); return -1; } struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ printf("appect error"); return -1; } char str[] = "Hello World"; write(clnt_sock, str, sizeof(str)); close(clnt_sock); close(server_sockfd); } ``` ## 客户端 客户端要和服务器进行通信,从流程图上可以看出,需要使用socket, connect, write, read, close这5个函数 ### socket函数 客户端要和服务端进行网络通讯,首先也必须调用socket函数,这里客户端也使用IPv4协议,使用字节流套接字,传输层选择TCP协议,所以第一段代码 ``` #include <stdio.h> #include <sys/socket.h> int main() { int sock_cli = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(sock_cli == -1){ printf("socket error"); return -1; } } ``` ### connect函数 TCP客户端就是使用connect函数和服务端建立连接,函数原型 ``` # sockfd 客户端TCP描述符 # sockaddr 服务端协议地址 # addrlen 服务端协议地址长度 int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen); ``` 这个函数将触发客户端和服务端三次握手,函数的第一个参数sockfd表示客户端返回的描述符,这里不需要调用bind函数绑定端口,系统会自动分配。函数的第二个参数需要配置服务端IP和端口信息,同样有结构体规范这些信息,结构体也是和服务端一样使用sockaddr_in类型。所以第二段代码 ``` #include <stdio.h> #include <sys/socket.h> int main() { int sock_cli = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(sock_cli == -1){ printf("socket error"); return -1; } struct sockaddr_in servaddr; servaddr.sin_family = AF_INET;/*使用IPv4协议*/ servaddr.sin_port = htons(8887);/*需要连接的远程服务器端口*/ servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*需要连接的远程服务器IP*/ if(connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){ printf("connect error"); return -1; } } ``` ### read函数 客户端连接上服务器之后返回的是一个Socket文件描述符,既然是文件描述符,就可以通过简单的read函数获取网络数据,read函数原型 ``` # sockdf 文件描述符 # buf 文件内容存放地址 # count 内容长度 ssize_t read(int sockfd,void *buf,size_t count) ``` 这里我们读取64个字节就够了,不需要太多 ``` char str[64]; read(sock_cli, str, 64); ``` ### 完整的客户端代码 ``` #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> int main() { int sock_cli = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(sock_cli == -1){ printf("socket error"); return -1; } struct sockaddr_in servaddr; servaddr.sin_family = AF_INET;/*使用IPv4协议*/ servaddr.sin_port = htons(8887);/*需要连接的远程服务器端口*/ servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*需要连接的远程服务器IP*/ if(connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){ printf("connect error"); return -1; } char str[64]; read(sock_cli, str, 64); printf(str); close(sock_cli); } ``` ## 运行客户端服务端 将我们的服务端代码保存为server.c,将我们的客户端代码保存为client.c。分别编译客户端和服务端代码 ``` [root@iZ940ofmvruZ socket]# gcc server.c -o server [root@iZ940ofmvruZ socket]# gcc client.c -o client ``` 然后会分别生成两个可执行文件server和client。在一个窗口中先执行server ``` [root@iZ940ofmvruZ socket]# ./server ``` 执行server之后,我们知道accept函数会阻塞,所以程序一直运行,等待客户端连接进来。这时候在另一个窗口执行客户端 ``` [root@iZ940ofmvruZ socket]# ./client Hello World ``` 可以看到服务端给我们发送的Hello World,我们再回到服务端执行窗口时,服务端也终止了进程,这个交互完成。然后我们会发现一个问题,服务端在提供完服务之后,它自己也关闭了,很显然,我们希望服务器是一致运行的提供服务,所以我们需要实现一直运行的服务器 ## 不间断提供服务 让服务器一直运行的方式很简单,就是死循环。循环的过程是accept一个客户端连接,然后处理数据请求,最后关闭客户端连接。注意,我们不能关闭服务端连接。所以我们改进这个程序,让server不间断的调用accept,因为accept总是从已连接的队列中返回一个连接,然后处理。改进内容片断 ``` /** * 进入死循环调用accept,给每一个连接上来的客户端发送Hello World */ for( ; ; ){ struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ printf("appect error"); return -1; } char str[] = "Hello World"; write(clnt_sock, str, sizeof(str)); close(clnt_sock); /*close(server_sockfd);*/ } ``` 这样修改之后,这个服务端程序就是一直不间断提供服务了 ## 并发服务器 ### 迭代服务器 我们首先来看下什么是迭代服务器,因为我们刚才所写的就是一个迭代服务器,思考一个问题,假如我们的服务器不是输出Hello World这么简单,而是需要经过一系列复杂逻辑计算甚至网络调用,那我们的程序执行起来就不会怎么快了,为了模拟这种场景,我们在服务端程序中假如sleep函数,我们让程序睡眠3秒钟,模拟服务器处理复杂逻辑时间 ``` #include <stdio.h> #include <unistd.h> #include <netinet/in.h> #include <sys/socket.h> int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); return -1; } if(listen(server_sockfd, 20) == -1){ printf("listen error"); return -1; } for( ; ; ){ struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ printf("appect error"); return -1; } char str[] = "Hello World"; sleep(3);//3秒之后再向客户端发送数据 write(clnt_sock, str, sizeof(str)); close(clnt_sock); /*close(server_sockfd);*/ } } ``` 运行这个服务端程序之后,我们同时执行10个客户端 ``` for(( i=0; i< 10; i++ )) do { ./client }& done ``` 在shell中执行这段代码,你会发现每隔3秒钟输出一个Hello World。这是因为我们的服务端程序是阻塞的,在处理一个请求的同时,其他请求只能等。所以最后一个客户端连接需要等到30秒才能收到服务端的输出。我们称这种服务器为迭代服务器,**迭代服务器会依次处理客户端的连接,只要当前连接的任务没有完成,服务器的进程就会一直被占用,直到任务完成后,服务器关闭这个socket,释放连接**。这显然不是我们想要的,我们希望每一个Hello Wrold都在3秒钟后马上输出。 ### 并发服务器 当一个服务处理客户端请求需要花费较长时间,但是我们又不希望整个服务器被单个客户端长期占用,而是希望同时服务多个客户。Unix中编写并发服务器最简单的办法就是fork一个子进程来服务每个客户。利用fork函数可以把处理客户端请求的任务交接到子进程,这样就实现多进程并发,我们可以写出这样服务器的轮廓 ``` pid_t pid; for( ; ; ){ struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ printf("appect error"); return -1; } /** * 这一段直接fork一个子进程 * 子进程处理单独处理完请求之后退出 */ if( (pid = fork()) == 0 ){ close(server_sockfd);/*子进程不需要监听,关闭*/ doit(clnt_sock);/*针对已连接的客户端套接字进行读写*/ close(clnt_sock);/*处理完毕,关闭客户端连接*/ exit(0);/*自觉退出*/ } close(clnt_sock); /*连接已经交由子进程处理,父进程可以关闭客户端连接了*/ /*close(server_sockfd);*/ } ``` 其中,doit函数我们先不实现,我们来看一下一个并发服务器处理一个客户端连接的流程 1. 服务器阻塞于accept调用且来自客户的连接请求到达时的客户端与服务器的状态 ![image](http://note.youdao.com/yws/api/personal/file/WEBb7219fa222a86827ce4b75df6bf606ca?method=download&shareKey=48c49fdea5bb2fbb25cb9996d675efd9) 2. 从accept返回后,连接已经在内核中注册,并且新的套接口connfd被创建。这是一个已建起连接的套接口,可以进行数据的读写。 ![image](http://note.youdao.com/yws/api/personal/file/WEB7c4d9e28990fdf6f96ad73199102ae2f?method=download&shareKey=141a216d06a156f66fbdf1b278119aca) 3.并发服务器在调用fork之后,listenfd和connfd这两个描述字在父进程以及子进程之间共享(实际为其中一份为copy) ![image](http://note.youdao.com/yws/api/personal/file/WEB1a9fc48f2c0d5528315d92d31e3ac6ff?method=download&shareKey=c863897204153764bdda8746ebf4cc84) 4. 接下来是由父进程关闭已连接套接口(connfd),由子进程关闭监听套接口(listenfd)。然后由子进程负责为客户端提供服务 ![image](http://note.youdao.com/yws/api/personal/file/WEBeb7883cfe2218b5d0c01daf78458ab50?method=download&shareKey=cc9f575193b603a6e7ef89cf8906c1bc) 最终我们的并发服务器代码为 ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <sys/socket.h> void doit(int sockfd); int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); pid_t pid; if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); return -1; } if(listen(server_sockfd, 20) == -1){ printf("listen error"); return -1; } for( ; ; ){ struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ printf("appect error"); return -1; } if( (pid = fork()) == 0 ){ close(server_sockfd);/*子进程不需要监听,关闭*/ doit(clnt_sock);/*针对已连接的客户端套接字进行读写*/ close(clnt_sock);/*处理完毕,关闭客户端连接*/ exit(0);/*自觉退出*/ } close(clnt_sock); /*close(server_sockfd);*/ } } void doit(int sockfd){ char str[] = "Hello World"; sleep(3);//3秒之后再向客户端发送数据 write(sockfd, str, sizeof(str)); } ``` 这个时候再次利用shell并行执行我们的客户端,就会发现,所有的Hello World是同时输出来的。这种服务器就可以做到快速同时处理多个客户端连接。
Pre:
服务器三种并发方式
Next:
MySQL事务和隔离级别
0
likes
1106
Weibo
Wechat
Tencent Weibo
QQ Zone
RenRen
Submit
Sign in
to leave a comment.
No Leanote account?
Sign up now.
0
comments
More...
Table of content
No Leanote account? Sign up now.