网络编程--并发服务器模型

文章目录

  • 并发服务器模型
  • 多进程
  • 多线程解决方案
    • 读写大量文件比较高效的方法
    • 多线程模型
  • 调用 fcntl 将 sockfd 设置为非阻塞模式

并发服务器模型


并发服务器模型的实现主要有三种方法:

  1. 多进程:此方法开销较大,不常用;
  2. 多线程:开销较小,常用;
  3. 调用 fcntl 将 sockfd 设置为非阻塞模式:

多进程


修改tcp_net_server.ctcp_net_server_multiprocess的代码如下:

#include "tcp_net_socket.h"

int main(int argc, char * argv[])
{
    if(argc < 3) {
        printf("usage:./servertcp ip port\n");
        exit(-1);
    }

    // 注册信号
    signalhandler();

    // 初始化socket
    int sfd = tcp_init(argv[1], atoi(argv[2]));

    // 用while循环表示可以与多个客户端接收和发送,但仍然是阻塞式的
    while(1) {
        // 接收客户端的连接
        int cfd = tcp_accept(sfd); // 如果没有连接请求阻塞
        char buf[512] = {0};

        if(0 == fork()) { // 开辟一个新进程出来请求
            // 与客户端进行通信
            if(-1 == recv(cfd, buf, sizeof(buf), 0)) {
                perror("recv");
                close(cfd);
            }
            else {
                printf("%s: %d: 正在处理...\n", __FILE__, __LINE__);
                puts(buf); usleep(5000000);
                printf("%s: %d: 处理完成...\n", __FILE__, __LINE__);

                // 发送数据
                if(-1 == send(cfd, "hello, world", 12, 0))
                    perror("send");

                close(cfd);
            }
        }
        else close(cfd);
    }

    close(sfd);

    return 0;
}

多线程解决方案


读写大量文件比较高效的方法


读大量文件:
比较高效的方法,每次读取指定大小的数据(除非读取结束),忽略中断的影响。下述方法仍然不够高效,因为要不断的进行系统调用。

ssize_t readn(int fd, char *buf, int size)
{
    /* 函数意义:读取大量文件
     * 参数意义:
     *     - int fd:文件描述符;
     *     - char *buf:存储数据的缓存空间;
     *     - int size:缓存空间 *buf 的大小
     * 返回值:
     *     - 如果读取成功,返回实际的数据大小;
     *     - 如果读取失败,返回 -1; 
     */
    char * pbuf = buf;
    int total, nread;
    for(total = 0; total < size; ) {
        nread = read(fd, pbuf, size - total);
        if (0 == nread) return total;
        else if (-1 == nread) {
            if (EINTR == errno) continue; // 忽略中断的影响
            else return -1;
        }
        else {
            total += nread;
            pbuf += nread;
        }
    }
    return total;
}

写大量文件:
比较高效的方法,每次写入指定大小的数据,忽略中断的影响。下述方法仍然不够高效,因为要不断的进行系统调用;

ssize_t writen(int fd, char * buf, int size)
{
    /* 函数意义:写入大量文件
     * 参数意义:
     *      - int fd:文件描述符;
     *      - char *buf:存储数据的缓存空间;
     *      - int size:缓存空间 *buf 的大小
     * 返回值:
     *      - 如果写入成功,返回实际写入数据的大小;
     *      - 如果写入失败,返回 -1; 
     */
    char * pbuf = buf;
    int total, nwrite;
    for(total = 0; total < size; ) {
        nwrite = write(fd, pbuf, size - total);
        if (-1 == nwrite) {
            if (EINTR == errno) continue; // 忽略中断的影响
            else return -1;
        }
        total += nwrite;
        pbuf += nwrite;
    }
    return total;
}

多线程模型


实现一个功能:将服务器上的文件的内容全部发给客户端。
服务的实现:服务器名称为:tcp_net_server_multithread.c
注意:下述代码并没有进行线程相关资源的销毁工作。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEAFUALT_SVR_PORT 2828
#define FILE_MAX_LEN 64
char filename[FILE_MAX_LEN];

void printPerror(const char * str)
{
    printf("%s: %d: ", __FILE__, __LINE__);
    perror(str);
}

// 线程回调函数
static void * handle_client(void * arg)
{
    /* 函数意义:线程的回调函数
     * 参数意义:
     *      - void *arg:Socket 的描述符,需要进行强制类型转化;
     * 返回值:
     *      - 无返回值;
     */
    int sock = *(int*)arg;
    char buff[1024];
    int len;
    printf("begin send\n");
    FILE * file = fopen(filename, "r");
    if(!file) {
        close(sock);
        exit(1);
    }

    // 发送文件名
    if(-1 == send(sock, filename, FILE_MAX_LEN, 0)) {
        perror("send file name\n");
        goto EXIT_THREAD;
    }

    printf("begin send file %s ...\n", filename);
    while(!feof(file)) {
        len = fread(buff, 1, sizeof(buff), file);
        printf("%s: %d: server read %s, len %d\n", __FILE__, __LINE__, filename, len);
        if(send(sock, buff, len, 0) < 0) {
            printPerror("send file: ");
            goto EXIT_THREAD;
        }
    }

    EXIT_THREAD:
    if(file) fclose(file);
    close(sock);
}

int main(int argc, char *argv[])
{
    int sockfd, new_fd;

    // 申请两个 ipv4的地址
    struct sockaddr_in my_addr;
    struct sockaddr_in their_addr;
    int sin_size, numbytes;
    pthread_t cli_thread; // 线程句柄
    unsigned short port;

    if(argc < 2) {
        printf("need a filename without path\n");
        exit(1);
    }
    strncpy(filename, argv[1], FILE_MAX_LEN);

    port = DEAFUALT_SVR_PORT;
    if(argc > 3) port = (unsigned short)atoi(argv[2]);

    // 第一步:建立TCP套接字Socket
    if(-1 == (sockfd = socket(AF_INET, SOCK_STREAM, 0))) {
        printPerror("socket");
        exit(-1);
    }

    // 第二步:设置侦听端口
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(port);
    my_addr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址可以通信

    // 第三步:绑定套接字,把Socket队列与端口关联起来
    if(-1 == bind(sockfd, (struct sockaddr * )&my_addr, sizeof(struct sockaddr))) {
        printPerror("bind");
        goto EXIT_MAIN;
    }

    // 第四步:开始在端口上帧听,是否有客户端发来连接
    if(-1 == listen(sockfd, 10)) {
        printPerror("listen");
        goto EXIT_MAIN;
    }

    printf("#@ listen port %d\n", port);
    // 第五步:循环与客户端通信
    while(1) {
        printf("%s: %d: server waitng ...\n", __FILE__, __LINE__);
        sin_size = sizeof(struct sockaddr_in);
        // 如果有客户端建立连接,将产生一个全新的套接字new_fd, 用于专门跟这些客户通信
        if((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {
            printPerror("accept:");
            goto EXIT_MAIN;
        }

        printf("---client (ip=%s:port=%d) request \n",inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port));

        // 生成一个线程完成和客户端会话, 父进程继续监听
        pthread_create(&cli_thread, NULL, handle_client, (void *)&new_fd);
    }

    // 第六步:关闭Socket
    EXIT_MAIN:
    close(sockfd);
    return 0;
}

客户端实现:文件名为tcp_net_client_multithread.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEAFUALT_SVR_PORT 2828
#define FILE_MAX_LEN 64

void printPerror(const char * str)
{
    printf("%s: %d: ", __FILE__, __LINE__);
    perror(str);
}

int main(int argc, char *argv[])
{
    int sockfd, numbytes;
    char buf[1024], filename[FILE_MAX_LEN];
    char ip_addr[64];
    struct hostent * he;
    struct sockaddr_in their_addr;
    int i = 0, len, total;
    unsigned short port;
    FILE * file = NULL;

    if(argc < 2) {
        printf("need a server ip \n");
        exit(1);
    }
    strncpy(ip_addr, argv[1], sizeof(ip_addr));
    port = DEAFUALT_SVR_PORT;
    if(argc > 3) port = (unsigned short)atoi(argv[2]);

    // 域名解析
    // he = gethostbyname(argv[1])

    // 第一步:建立TCP套接字Socket
    if(-1 == (sockfd = socket(AF_INET, SOCK_STREAM, 0)))
    {
        printPerror("socket");
        exit(-1);
    }

    // 第二步:设服务器地址和端口
    memset(&their_addr, 0, sizeof(their_addr));
    their_addr.sin_family = AF_INET;
    their_addr.sin_port = htons(port);
    their_addr.sin_addr.s_addr = inet_addr(ip_addr);
    // their_addr.sin_addr = *((struct in_addr *)he->h_addr);
    // bzero(&(their_addr.sin_zero), 8)

    // 第三步:用connect和服务器建立连接,这里没有使用本地端口,由协议栈自动分配端口
    printf("%s: %d: connect server %s: %d\n", __FILE__, __LINE__, ip_addr, port);
    if(-1 == connect(sockfd, (struct sockaddr * )&their_addr, sizeof(struct sockaddr))) {
        printPerror("connet");
        exit(1);
    }

    // 发生数据
    if(send(sockfd, "hello", 6, 0) < 0) {
        printPerror("send ");
        exit(1);
    }

    // 接收文件名,为编程方便,假设前64字节固定是文件名,不足用0来填充
    total = 0;
    while(total < FILE_MAX_LEN) {
        // 注意这里的接收buffer长度,始终是未接收文件名剩下长度
        len = recv(sockfd, filename+total, (FILE_MAX_LEN - total), 0);
        if (len <= 0) break;
        total += len;
    }

    // 接收文件名出错
    if(total != FILE_MAX_LEN) {
        printPerror("failure file name");
        exit(-3);
    }

    printf("%s: %d: recv file %s ...\n", __FILE__, __LINE__, filename);
    //file = fopen(filename, "wb");
    file = fopen("./abc.txt", "wb");
    if(!file) {
        printf("create file %s failure", filename);
        perror("create:");
        exit(-3);
    }

    // 接收文件数据
    printf("%s: %d: recv begin\n", __FILE__, __LINE__);
    total = 0;
    while(1) {
        len = recv(sockfd, buf, sizeof(buf), 0);
        if (len == -1) break;
        total += len;
        fwrite(buf, 1, len, file);
    }
    fclose(file);
    printf("%s: %d: recv file %s success total length %d\n", __FILE__, __LINE__, filename, total);

    close(sockfd);
    return 0;
}

调用 fcntl 将 sockfd 设置为非阻塞模式


#include 
#include 
......
// 建立 Socket 
sockfd = socket(AF_INET, SOCK_STREAM, 0);
...
int iflags;
if((iflags = fcntl(sfd, F_GETFL, 0)) < 0 ) { // 获取 sfd 的模式信息
    printf("%s: %d: ", __FILE__, __LINE__);
    perror("fcntl F_GETFL");
}
iflags |= O_NONBLOCK; // 设置为非阻塞模式
// iflags |= O_ASYNC;
if(fcntl(sfd, F_SETFL, iflags) < 0) { // 写入修改后的模式信息
    printf("%s: %d: ", __FILE__, __LINE__);
    perror("fcntl");
}

通过调用 fcntlsockfd 设置为非阻塞模式,在遇到多客户端请求时,会有什么表现?
服务器在执行到函数 accept() 时,会立即返回:accept: Resource temporarily unavailable

  • 默认的 sockfd 为阻塞模式,在 accept 函数执行中会阻塞直到等到客户端的连接请求时返回;
  • 当把 sockfd 设置为非阻塞模式时,accept 模式会立即返回,即使没有等到客户端的连接。这样就可以去处理其他任务;

你可能感兴趣的:(Linux编程)