1. 程式人生 > >併發伺服器三種實現方式之程序、執行緒和select

併發伺服器三種實現方式之程序、執行緒和select

  前言:剛開始學網路程式設計,都會先寫一個客戶端和服務端,不知道你們有沒有試一下:再開啟一下客戶端,是連不上服務端的。還有一個問題不知道你們發現沒:有時啟伺服器,會提示“Address already in use”,過一會就好了,想過為啥麼?在這篇部落格會解釋這個問題。

  但現實的伺服器都會連很多客戶端的,像阿里伺服器等,所以這篇主要介紹如何實現併發伺服器,主要通過三種方式:程序、執行緒和select函式來分別實現。

  一、程序實現併發伺服器

  先說下什麼是併發伺服器吧?不是指有多個伺服器同時執行,而是可以同時連線多個客戶端。

  先簡單說下原理吧,先畫個圖,如下:  PS:全部落格園最醜圖,不接受反駁!哈哈哈

  

  先要搞清楚通訊的流程,圖上引數說明:

  lfd:socket函式的返回值,就是監聽描述符

  cfd1/cfd2/cfd3:accept函式的返回值,用通訊的套接字

  server:伺服器

  client:客戶端

  socket通訊過程中,總共有幾個套接字呢?答:三個,客戶端一個,伺服器兩個。

  根據上圖來大致說明一下流程:

  客戶端建立一個套接字描述符,用於通訊,伺服器先用socket函式建立套接字,用於監聽客戶端,然後呼叫accept函式,會返回一個套接字,用於通訊的。圖上就是,client1先通過cfd與server建立連線,然後與cfd1建立連線通訊,這時lfd就空閒了,再監聽客戶端,client2再與lfd連線,再跟cfd2通訊。client3也是如此。

  現在問題就是。如何建立多個程序與客戶端通訊呢?通過迴圈建立子程序就可以實現這個問題,可以參考我的這篇部落格:https://www.cnblogs.com/liudw-0215/p/9667686.html

  服務端程式,如下:

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <ctype.h>
#include 
<unistd.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 void do_sigchild(int num) { while (waitpid(0, NULL, WNOHANG) > 0) ; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立訊號,處理子程序退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); // int opt = 1; //setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //埠複用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0; }
View Code

  客戶端程式如下:

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (fgets(buf, MAXLINE, stdin) != NULL) {
        Write(sockfd, buf, strlen(buf));
        n = Read(sockfd, buf, MAXLINE);
        if (n == 0) {
            printf("the other side has been closed.\n");
            break;
        }
        else
            Write(STDOUT_FILENO, buf, n);
    }

    Close(sockfd);

    return 0;
}
View Code

  演示效果,伺服器可以同時處理兩個客戶端,如下:

  

  但我想再測試一下程式,執行./server,發現有個bind error,如下:

  

  下面來解釋一下這個問題:

  先來一張圖片(出自UNP),如下:

  

  這張圖將三次握手、四次揮手和TCP狀態轉換圖,這些在我的這篇部落格都由介紹,可以參考一下:https://www.cnblogs.com/liudw-0215/p/9661583.html

注意最後有一個TIME_WAIT狀態,主動關閉一端會經歷2MSL時長等待(大約40秒),再變為最開始的狀態CLOSED。

  復現上面的“bind error”,只需退出伺服器,在啟伺服器,就會報出此錯。因為主動關閉一端,會經歷2MSL時長,埠IP會被佔用,所以會報“bind error”。

但可能會問:為啥先退出客戶端沒有此問題?因為客戶端沒有呼叫bind函式地址結構,會“隱式”生成埠。

  有沒有方法可以解決這個問題呢?當然有的,呼叫函式setsockopt即可,服務端程式如下:

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <ctype.h>
#include <unistd.h>

#include "wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

void do_sigchild(int num)
{
    while (waitpid(0, NULL, WNOHANG) > 0)
        ;
}

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int i, n;
    pid_t pid;
    struct sigaction newact;

    newact.sa_handler = do_sigchild;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGCHLD, &newact, NULL);    //建立訊號,處理子程序退出

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));    //埠複用

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    Listen(listenfd, 20);

    printf("Accepting connections ...\n");
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        pid = fork();
        if (pid == 0) {
            Close(listenfd);
            while (1) {
                n = Read(connfd, buf, MAXLINE);
                if (n == 0) {
                    printf("the other side has been closed.\n");
                    break;
                }
                printf("received from %s at PORT %d\n",
                        inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
                for (i = 0; i < n; i++)
                    buf[i] = toupper(buf[i]);
                Write(STDOUT_FILENO, buf, n);
                Write(connfd, buf, n);
            }
            Close(connfd);
            return 0;
        } else if (pid > 0) {
            Close(connfd);
        }  else
            perr_exit("fork");
    }
    return 0;
}
View Code

  二、執行緒實現併發伺服器

  理解了程序的方式,就是建立多個執行緒來實現,就不過多解釋了,程式需要對執行緒有一定了解,之後我還會寫篇部落格來介紹執行緒,敬請期待哦。

  伺服器程式碼如下,有有詳細的解釋:

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>

#include "wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

struct s_info {                     //定義一個結構體, 將地址結構跟cfd捆綁
    struct sockaddr_in cliaddr;
    int connfd;
};

void *do_work(void *arg)
{
    int n,i;
    struct s_info *ts = (struct s_info*)arg;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];      //#define INET_ADDRSTRLEN 16  可用"[+d"檢視

    while (1) {
        n = Read(ts->connfd, buf, MAXLINE);                     //讀客戶端
        if (n == 0) {
            printf("the client %d closed...\n", ts->connfd);
            break;                                              //跳出迴圈,關閉cfd
        }
        printf("received from %s at PORT %d\n",
                inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
                ntohs((*ts).cliaddr.sin_port));                 //列印客戶端資訊(IP/PORT)

        for (i = 0; i < n; i++) 
            buf[i] = toupper(buf[i]);                           //小寫-->大寫

        Write(STDOUT_FILENO, buf, n);                           //寫出至螢幕
        Write(ts->connfd, buf, n);                              //回寫給客戶端
    }
    Close(ts->connfd);

    return (void *)0;
}

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    pthread_t tid;
    struct s_info ts[256];      //根據最大執行緒數建立結構體陣列.
    int i = 0;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);                     //建立一個socket, 得到lfd

    bzero(&servaddr, sizeof(servaddr));                             //地址結構清零
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);                   //指定本地任意IP
    servaddr.sin_port = htons(SERV_PORT);                           //指定埠號 8000

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //繫結

    Listen(listenfd, 128);      //設定同一時刻連結伺服器上限數

    printf("Accepting client connect ...\n");

    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);   //阻塞監聽客戶端連結請求
        ts[i].cliaddr = cliaddr;
        ts[i].connfd = connfd;

        /* 達到執行緒最大數時,pthread_create出錯處理, 增加伺服器穩定性 */
        pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
        pthread_detach(tid);                                                    //子執行緒分離,防止僵執行緒產生.
        i++;
    }

    return 0;
}
View Code

  客戶端程式碼如下:

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr.s_addr);
    servaddr.sin_port = htons(SERV_PORT);

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (fgets(buf, MAXLINE, stdin) != NULL) {
        Write(sockfd, buf, strlen(buf));
        n = Read(sockfd, buf, MAXLINE);
        if (n == 0)
            printf("the other side has been closed.\n");
        else
            Write(STDOUT_FILENO, buf, n);
    }

    Close(sockfd);

    return 0;
}
View Code

  三、select實現併發伺服器

  select和程序主要區別在於,程序是阻塞的,而select是交給核心自己來實現的,由於select比較複雜,參考我的另一篇部落格:https://www.cnblogs.com/liudw-0215/p/9661583.html

  總結:有不懂的,歡迎及時評論。