一个好的服务器,一般都是并发服务器(同一时刻可以响应多个客户端的请求)。并发服务器设计一般有多进程服务器、多线程服务器、I/O复用服务器等。
一、进程版本
优点:父子进程资源独立,某个进程结束,不会影响已有的进程,服务器更加稳定。
缺点:消耗资源大
demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/************************************************************************
函数名称: void main(int argc, char *argv[])
函数功能: 主函数,用进程建立一个TCP Echo Server
函数参数: 无
函数返回: 无
************************************************************************/
int main(int argc, char *argv[])
{
unsigned short port = 8080; // 本地端口
//1.创建tcp套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror( "socket");
exit( -1);
}
//配置本地网络信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr)); // 清空
my_addr.sin_family = AF_INET; // IPv4
my_addr.sin_port = htons(port); // 端口
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip
//2.绑定bind
int err_log = bind(sockfd, ( struct sockaddr*)&my_addr, sizeof(my_addr));
if( err_log != 0)
{
perror( "binding");
close(sockfd);
exit( -1);
}
//3.监听listen
err_log = listen(sockfd, 10);
if(err_log != 0)
{
perror( "listen");
close(sockfd);
exit( -1);
}
while( 1) //主进程 循环等待客户端的连接
{
char cli_ip[INET_ADDRSTRLEN] = {0}; //INET_ADDRSTRLEN定义为16
struct sockaddr_in client_addr;
socklen_t cliaddr_len = sizeof(client_addr);
// 取出客户端已完成的连接
int connfd = accept(sockfd, ( struct sockaddr*)&client_addr, &cliaddr_len);
if(connfd < 0)
{
perror( "accept");
close(sockfd);
exit( -1);
}
//创建fork
pid_t pid = fork();
if(pid < 0)
{
perror( "fork");
_exit( -1);
}
//子进程 接收客户端的信息,并发还给客户端
else if( 0 == pid)
{
/*关闭不需要的套接字可节省系统资源,
同时可避免父子进程共享这些套接字
可能带来的不可预计的后果
*/
close(sockfd); // 关闭监听套接字,这个套接字是从父进程继承过来
char recv_buf[ 1024] = { 0};
int recv_len = 0;
// 打印客户端的 ip 和端口
memset(cli_ip, 0, sizeof(cli_ip)); // 清空
inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
printf( "----------------------------------------------\n");
printf( "client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
// 接收数据
while( (recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0 )
{
printf( "recv_buf: %s\n", recv_buf); // 打印数据
send(connfd, recv_buf, recv_len, 0); // 给客户端回数据
}
printf( "client_port %d closed!\n", ntohs(client_addr.sin_port));
close(connfd); //关闭已连接套接字
exit( 0);
}
// 父进程
else if(pid > 0)
{
close(connfd); //关闭已连接套接字
}
}
close(sockfd);
return 0;
}
网络调试助手登录:
二、线程版本
优点:线程共享进程资源,资源开销小。
缺点:一旦主进程结束,所以进程都会结束,不稳定。
demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
/************************************************************************
函数名称: void *client_fun(void *arg)
函数功能: 线程函数,处理客户信息
函数参数: 已连接套接字
函数返回: 无
************************************************************************/
void *client_fun(void *arg)
{
int recv_len = 0;
char recv_buf[1024] = ""; // 接收缓冲区
int connfd = (int)arg; // 传过来的已连接套接字
// 接收数据
while((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0)
{
printf("recv_buf: %s\n", recv_buf); // 打印数据
send(connfd, recv_buf, recv_len, 0); // 给客户端回数据
}
printf("client closed!\n");
close(connfd); //关闭已连接套接字
return NULL;
}
//===============================================================
// 语法格式: void main(void)
// 实现功能: 主函数,建立一个TCP并发服务器
// 入口参数: 无
// 出口参数: 无
//===============================================================
int main(int argc, char *argv[])
{
int sockfd = 0; // 套接字
int connfd = 0;
int err_log = 0;
struct sockaddr_in my_addr; // 服务器地址结构体
unsigned short port = 8080; // 监听端口
pthread_t thread_id;
printf("TCP Server Started at port %d!\n", port);
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
if(sockfd < 0)
{
perror("socket error");
exit(-1);
}
bzero(&my_addr, sizeof(my_addr)); // 初始化服务器地址
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
printf("Binding server to port %d\n", port);
// 绑定bind
err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind");
close(sockfd);
exit(-1);
}
// 监听listen
err_log = listen(sockfd, 10);
if( err_log != 0)
{
perror("listen");
close(sockfd);
exit(-1);
}
printf("Waiting client...\n");
while(1)
{
char cli_ip[INET_ADDRSTRLEN] = ""; // 用于保存客户端IP地址
struct sockaddr_in client_addr; // 用于保存客户端地址
socklen_t cliaddr_len = sizeof(client_addr); // 必须初始化!!!
//获得一个已经建立的连接
connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);
if(connfd < 0)
{
perror("accept this time");
continue;
}
// 打印客户端的 ip 和端口
inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
printf("----------------------------------------------\n");
printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
if(connfd > 0)
{
//由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,值传递。
pthread_create(&thread_id, NULL, (void *)client_fun, (void *)connfd); //创建线程
pthread_detach(thread_id); // 线程分离,结束时自动回收资源
}
}
close(sockfd);
return 0;
}
三、select版本
进程方式和线程方式可以实现并发服务,其实里面调用send,read,accept函数都会导致阻塞。而linux的select函数可以使我们在程序中同时监听多个文件描述符的读写状态。程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变。select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组, 每一个数组元素都能与一打开的文件描述符(不管是Socket描述符,还是其他 文件或命名管道或设备描述符)建立联系, 当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
API:
FD_ZERO(fd_set *) 清空一个文件描述符集合;
FD_SET(int ,fd_set *) 将一个文件描述符添加到一个指定的文件描述符集合中;
FD_CLR(int ,fd_set*) 将一个给定的文件描述符从集合中删除;
FD_ISSET(int ,fd_set* ) 检查集合中指定的文件描述符是否可以读写。
select函数
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
参数1:maxfdp是一个整数,是指集合所以文件描述符的范围,即所以文件描述符的最大值+1;
参数2:fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符。我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
参数3:fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
参数4:fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常文件。
参数5:struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
返回值:
demo:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define MAX_LISTEN 5
#define PORT 8080
#define IP "192.168.199.208"
int main()
{
int conn_fd;
//1.socket创建套接字
int sock_fd = socket(AF_INET,SOCK_STREAM,0);
if (sock_fd < 0) {
perror("create socket failed");
exit(1);
}
struct sockaddr_in addr_client;
int client_size = sizeof(struct sockaddr_in);
//2.bind绑定服务端的IP和端口
struct sockaddr_in addr_serv;
memset(&addr_serv, 0, sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_port = htons(PORT);
addr_serv.sin_addr.s_addr = inet_addr(IP);
if (bind(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr_in)) < 0) {
perror("bind error");
exit(1);
}
//3.listen监听
if (listen(sock_fd,MAX_LISTEN) < 0) {
perror("listen failed");
exit(1);
}
int recv_num; //收到的数据长度
int send_num; //发送的数据长度
char recv_buf[100]; //收到的数据
char send_buf[100]; //发送的数据
//用一个数组记录描述符的状态
int i;
int ready;
int max_fd;
int client[FD_SETSIZE];
for (i = 0;i < FD_SETSIZE;i ++) {
client[i] = -1;
}
fd_set readset; //文件描述符集合
max_fd = sock_fd;
//最大可用描述符的个数,一般受操作系统内核的设置影响,我的环境下这个值是1024
printf("max fd num %d\n",FD_SETSIZE);
while (1) {
//重置监听的描述符
FD_ZERO(&readset); //清空文件描述符集合
FD_SET(sock_fd,&readset); //将一个文件描述符添加到一个指定的文件描述符集合中
for (i = 0;i < FD_SETSIZE;i ++) {
if (client[i] == 1) {
FD_SET(i, &readset);
}
}
//开始监听描述符,是异步的,不会阻塞
ready = select(max_fd+1, &readset, NULL, NULL, NULL);
//可用描述符如果是创建连接描述符,则创建一个新的连接
if (FD_ISSET(sock_fd, &readset)) {
conn_fd = accept(sock_fd, (struct sockaddr *)&addr_client, &client_size);
if (conn_fd < 0) {
perror("accept failed");
exit(1);
}
FD_SET(conn_fd, &readset);
FD_CLR(sock_fd, &readset);
if (conn_fd > max_fd) {
max_fd = conn_fd;
}
client[conn_fd] = 1;
}
//检查所有的描述符,查看可读的是哪个,针对它进行IO读写
for (i = 0; i < FD_SETSIZE; i ++) {
if (FD_ISSET(i, &readset)) {
recv_num = recv(i, recv_buf, sizeof(recv_buf), 0);
if (recv_num <= 0) {
FD_CLR(i, &readset); //将文件描述符从集合中清除
client[i] = -1;
}
recv_buf[recv_num] = '\0';
memset(send_buf,0,sizeof(send_buf));
sprintf(send_buf, "server proc got %d bytes\n", recv_num);
send_num = send(i, send_buf, strlen(send_buf), 0);
if (send_num <= 0) {
FD_CLR(i, &readset);
client[i] = -1;
}
}
}
}
close(sock_fd);
return 0;
}