《TCP/IP网络编程》(韩 尹圣雨)学习系列之3,核心项目总结

《TCP/IP网络编程》(韩 尹圣雨)学习系列之3,核心项目总结

《TCP/IP网络编程》(韩 尹圣雨)学习系列之1,学习笔记.
《TCP/IP网络编程》(韩 尹圣雨)学习系列之2,课后习题解答.
本文是我复习《TCP/IP网络编程》时所整理的,复习书中的几个主要的项目,包括回声服务器、聊天服务器、Web服务器。由于书中没有注释,我在代码中做了比较详细的注释,有助于大家理解代码的逻辑,理解服务器的原理、服务器编程的步骤,所有代码均在linux系统下验证过,都能跑通。各个项目复习的知识如下:
1、回声服务器(echo_server.c、echo_client.c、echo_client2.c),复习套接字编程。
2、多进程回声服务器(echo_mpserver.c、echo_mpclient.c),复习多进程相关知识、信号处理、I/O分割。
3、聊天服务器(chat_clnt.c、chat_server.c),复习多线程相关知识,锁机制、临界区等概念。
4、多线程Web服务器(webserv_linux.c),复习标准I/O、多线程服务器、HTTP协议等知识。

一、回声服务器
1、echo_server.c
/*
    程序名:echo_server.c
    功能:基于TCP的回声服务器端
    作者:fouries
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    int str_len, i;

    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

    if(argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    // Step1: 调用socket()函数,创建服务器端套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error!");

    // 初始化sockaddr_in结构体类型的变量serv_adr
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    
    // Step2: 调用bind()函数,给套接字分配地址
    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error!");

    // Step3: 调用listen()函数,使serv_sock进入可连接状态, 连接请求等待队列长度为5。(最多让5个连接请求进入队列)
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error!");

    clnt_adr_sz = sizeof(clnt_adr);
    
    for(i=0; i<5; i++)
    {   
        // Step4: 调用accept()函数,受理客户端的连接请求
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if(clnt_sock == -1)
            error_handling("accept() error!");
        else
            printf("Connected client %d \n", i+1);
        
        // Step5: 调用read()/write()函数进行数据交换
        while((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
            write(clnt_sock, message, str_len);

        close(clnt_sock);
    }

    // Step6: 调用close()函数关闭服务器端套接字
    close(serv_sock);
    return 0;
}

void error_handling( char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
2、echo_client.c
/*
    程序名:echo_client.c
    功能:基于TCP的回声客户端
        该客户端56-59行存在问题。
            错误1.每次调用read、write函数时都会以字符串为单位执行实际的I/O操作
            错误2.字符串太长,需要分2个数据包发送!
    作者:fouries
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling( char *message);

int main(int argc, char* argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;

    if(argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    // Step1: 调用socket()函数,创建客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error!");

    // sockaddr_in结构体类型的变量 serv_adr初始化
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    // Step2: 调用connect()函数,请求与服务器端连接
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
    else
        puts("Connected.....");
    
    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);

        if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        
        // Step3: 调用write()/read()函数,进行数据交换
        write(sock, message, strlen(message));
        str_len = read(sock, message, BUF_SIZE-1);
        message[str_len] = 0;   // 等价于 = '\0';
        printf("Message from server: %s", message);
    }
    
    // Step4: 调用close()函数,关闭客户端套接字
    close(sock);
    return 0;
}

void error_handling( char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

3、echo_client2.c
/*
    程序名:echo_client2.c
    功能:基于TCP的回声客户端2
         该客户端完美解决了发送数据量大时数据丢失的问题
    作者:fouries
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling( char *message);

int main(int argc, char* argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    if(argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    // Step1: 调用socket()函数,创建客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error!");

    // sockaddr_in结构体类型的变量 serv_adr初始化
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    // Step2: 调用connect()函数,请求与服务器端连接
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
    else
        puts("Connected.....");
    
    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);

        if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        
        // Step3: 调用write()/read()函数,进行数据交换
        str_len = write(sock, message, strlen(message));
        recv_len = 0;

        // 判断接收到的消息的长度是否等于发送了的消息的长度,当相等时退出while。(用 != 判断,容易引发异常造成无限循环,用 < 即使异常也不会无限循环)
        while(recv_len < str_len)
        {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE-1);
            if(recv_cnt == -1)
                error_handling("read() error!");
            recv_len += recv_cnt;
        }

        message[recv_len] = 0;   // 等价于 = '\0';
        printf("Message from server: %s", message);
    }
    
    // Step4: 调用close()函数,关闭客户端套接字
    close(sock);
    return 0;
}

void error_handling( char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
二、多进程回声服务器
1、echo_mpserver.c
/*
    程序名:echo_mpserver.c
    功能:该程序为多线程进程回声服务器的服务器端
        使用信号处理器清除终止了的子进程,多进程用于受理客户端的的连接请求
    作者:fouries
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;

    pid_t pid;
    struct sigaction act;   // 定义struct sigaction结构体变量act
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if(argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    // 初始化struct sigaction结构体变量act
    act.sa_handler = read_childproc; 
    sigemptyset(&act.sa_mask);  // act的struct sigset_t结构体类型变量sa_mask所有位初始化为0
    act.sa_flags = 0;           // act的int型成员变量sa_flags初始化为0

    // 注册信号,成功时返回0,失败时返回-1。
    state = sigaction(SIGCHLD, &act, 0);

    // 调用socket()函数,创建套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    
    // 初始化sockaddr_in结构体类型的变量serv_adr
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    // 调用bind()函数,为套接字分配地址
    if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error!");
    
    // 调用listen()函数,使serv_sock进入可连接状态, 连接请求等待队列长度为5。
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error!");
    
    while(1)
    {
        adr_sz = sizeof(clnt_adr);
        // 调用accept()函数,受理客户端的连接请求
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
        if(clnt_sock == -1)
            continue;   // 受理错误,退出重新进入while循环
        else
            puts("new client connected...");
        
        // 创建一个子进程,如果子进程创建失败,则关闭clnt_sock套接字,退出重新进入while循环
        pid = fork();   
        if(pid == -1)   
        {
            close(clnt_sock);
            continue;
        }

        if(pid == 0)    /*子进程运行区域*/
        {
            close(serv_sock);   // 关闭子进程中复制来的serv_sock套接字

            // 接收客户端发送的数据,并发送给客户端
            while((str_len=read(clnt_sock, buf, BUF_SIZE)) != 0)
                write(clnt_sock, buf, str_len);
            
            // 收发完毕,关闭clnt_sock套接字
            close(clnt_sock);
            puts("client disconnected...");
            return 0;
        }
        else    // 关闭父进程中的clnt_sock套接字
            close(clnt_sock);
    }
    close(serv_sock);    // 最后关闭父进程serv_sock套接字
    return 0;
}

// 信号处理器,清除终止的子进程,防止僵尸进程。
void read_childproc(int sig)
{
    int status;
    pid_t pid = waitpid(-1, &status, WNOHANG);  // 调用waitpid函数清除终止的子进程,返回终止子进程的ID
    printf("removed proc id: %d \n", pid);      // 打印被移除进程的ID
}

// 错误处理函数
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

2、echo_mpclient.c
/*
    程序名:echo_mpclient.c
    功能:该程序为多线程进程回声客户端
        回声客户端无特殊原因不需要分割I/O, 本程序用于讲解I/O分割的方法而选择回声客户端
        对回声客户端的读/写数据进行了分割。
    作者:fouries
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling( char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);

int main(int argc, char* argv[])
{
    int sock;
    pid_t pid;
    char buf[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    if(argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    // Step1: 调用socket()函数,创建客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error!");

    // sockaddr_in结构体类型的变量 serv_adr初始化
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    // Step2: 调用connect()函数,请求与服务器端连接
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
    
    // Step3: 调用write_routine/read_routine函数收发数据
    pid = fork();
    if(pid == 0)
        write_routine(sock, buf);
    else
        read_routine(sock, buf);
    
    // Step4: 调用close()函数,关闭客户端套接字
    close(sock);
    return 0;
}


// 分割I/O后的客户端接收数据函数
void read_routine(int sock, char *buf)
{
    while(1)
    {   
        // 读取服务器端返回的消息,并打印出来
        int str_len = read(sock, buf, BUF_SIZE);
        if(str_len == 0)
            return;

        buf[str_len] = 0;
        printf("Message from server: %s", buf);
    }
}

// 分割I/O后的客户端发送数据函数
void write_routine(int sock, char *buf)
{
    while(1)
    {
        // 输入需要发送的消息,并发送给服务器端
        fgets(buf, BUF_SIZE, stdin);
        if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
        {
            shutdown(sock, SHUT_WR);    // TCP的半关闭,断开输出流,向服务器端发送EOF,还能用read_routine()接收数据
            return;                     // 返回空,void返回类型也可以什么都不写
        }
        write(sock, buf, strlen(buf));  // 半关闭之后无法发送数据
    }
}

void error_handling( char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

三、聊天服务器
1、chat_server.c
/*
    程序名:chat_server.c
    功能:为多个聊天客户端提供服务
        使用用多线程和锁机制(互斥量), 重点掌握临界区的构成。
        “访问全局变量clnt_cnt和数组clnt_socks的代码将构成临界区!”
    作者:fouries
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define BUF_SIZE 100
#define MAX_CLNT 256

void *handle_clnt(void *arg);
void send_msg(char *msg, int len);
void error_handling( char *message);

int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;                           // 声明pthread_mutex_t型变量,用来保存操作系统创建的互斥量(锁系统)。

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int clnt_adr_sz;
    pthread_t t_id;                             // 创建一个pthread_t类型变量t_id,用于保存新创建线程ID的变量地址值。
    if(argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    // 创建互斥量
    pthread_mutex_init(&mutx, NULL);
    
    // 创建socket套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    // 初始化sockaddr_in结构体类型的变量serv_adr
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    // 给套接字分配地址
    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error!");
    
    // 使serv_sock进入可连接状态, 连接请求等待队列长度为5。
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error!");

    while(1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        // 受理客户端的连接请求
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);

        // 利用互斥量锁住并释放临界区
        pthread_mutex_lock(&mutx);
        clnt_socks[clnt_cnt++] = clnt_sock;     // 临界区, 将受理连接的客户端文件描述符保存于clnt_socks数组中
        pthread_mutex_unlock(&mutx);

        // 创建一个线程,从handle_clnt函数调用开始,在单独的执行流中运行。同时在调用handle_clnt函数时向其传递clnt_sock变量的地址值。
        pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);

        // pthread_join:调用该函数的线程将进入阻塞状态,等待结束后回收其资源
        // pthread_detach:调用该函数不会引起线程阻塞,线程结束后会自动释放所有资源
        pthread_detach(t_id);
        printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
    }
    // 关闭服务器端套接字
    close(serv_sock);
    return 0;
}

// t_id线程的main函数,给客户端提供服务的函数
void *handle_clnt(void *arg)
{
    int clnt_sock = *((int*)arg);  // handle_clnt的参数为主进程的clnt_sock变量,类型为void*
    int str_len = 0, i;
    char msg[BUF_SIZE];

    // 接收客户端发送过来的消息,并给所有客户端都发送该消息
    while((str_len=read(clnt_sock, msg, sizeof(msg))) != 0)
        send_msg(msg, str_len);

// 利用互斥量锁住并释放临界区
    pthread_mutex_lock(&mutx);
    // 清除无连接的客户端
    // 临界区起始
    for(i=0; i<clnt_cnt; i++)
    {
        // 遍历整个clnt_socks数组,当传给进程的套接字的文件描述符与连接上的套接字的文件描述符相等时
        if(clnt_sock == clnt_socks[i]) 
        {
            // 将clnt_socks数组从索引i+1到clnt_cnt-1的套接字文件描述符整体向前移动一位(覆盖了重复的clnt_socks[i])。i最大为clnt_clnt-1
            while(i++<clnt_cnt-1)
                clnt_socks[i] = clnt_socks[i+1];
            break;      // 退出循环
        }
    }
    clnt_cnt--;         // 清除了clnt_socks里一个套接字之后,将clnt_cnt存储的数量减 1
    // 临界区结束
    pthread_mutex_unlock(&mutx);
    
    // 关闭客户端的套接字
    close(clnt_sock);               
    return NULL;    
}

// 给所有客户端发送消息的函数
void send_msg(char *msg, int len)   
{
    int i;

    // 利用互斥量锁住并释放临界区
    pthread_mutex_lock(&mutx);
    for(i=0; i<clnt_cnt; i++)               // 临界区
        write(clnt_socks[i], msg, len);     // 临界区
    pthread_mutex_unlock(&mutx);
}

void error_handling( char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
2、chat_clnt.c
/*
    程序名:chat_clnt.c
    功能:多人聊天室的客户端
         客户端为了分离输入和输出过程而创建了线程。
    作者:fouries
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define BUF_SIZE 100
#define NAME_SIZE 20

void *send_msg(void *arg);
void *recv_msg(void *arg);
void error_handling( char *message);

char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];

int main(int argc, char* argv[])
{
    int sock;
    struct sockaddr_in serv_adr;
    pthread_t snd_thread, rcv_thread;
    void *thread_return;
    if(argc != 4)
    {
        printf("Usage : %s <IP> <port> <name>\n", argv[0]);
        exit(1);
    }

    sprintf(name, "[%s]", argv[3]);
    // 创建客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error!");

    // sockaddr_in结构体类型的变量 serv_adr初始化
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    // 请求与服务器端连接
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
    
    // 分别创建snd_thread和rcv_thread线程
    pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);
    pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);
    // 分别等待snd_thread和rcv_thread线程结束,thread_return保存线程的main函数返回值
    pthread_join(snd_thread, &thread_return);
    pthread_join(rcv_thread, &thread_return);

    // 关闭客户端套接字
    close(sock);
    return 0;
}

// snd_thread线程的main函数,用于客户端发送数据
void *send_msg(void *arg)
{
    int sock = *((int*)arg);
    // 声明字符数组name_msg,保存姓名字符串和消息字符串
    char name_msg[NAME_SIZE+BUF_SIZE];
    while(1)
    {   
        // 接收客户端从终端输入的字符串消息,保存于msg字符数组中
        fgets(msg, BUF_SIZE, stdin);
        // 判断输入的字符串是否是“Q\n”或“q\n”
        if(!strcmp(msg, "q\n")||!strcmp(msg, "Q\n"))
        {
            close(sock);
            exit(0);        // 立即终止调用进程(关掉主进程的I/O),并向主进程返回 0。
        }
        // 将name和msg数组中的数据以字符串的格式输出到name_msg数组中
        sprintf(name_msg, "%s %s", name, msg);
        // 将name_msg数组中的字符发送到服务器端套接字
        write(sock, name_msg, strlen(name_msg));
    }
    return NULL;
}

// rcv_thread线程的main函数,用于接收数据
void *recv_msg(void *arg)
{
    int sock = *((int*)arg);
    // 声明字符数组name_msg,保存姓名字符串和消息字符串
    char name_msg[NAME_SIZE+BUF_SIZE];
    int str_len;
    while(1)
    {
        // 接收服务器端发送的数据,并将数据保存于 name_msg之中
        str_len = read(sock, name_msg, NAME_SIZE+BUF_SIZE-1);
        // 如果接收失败,则该线程返回(void*)类型的-1
        if(str_len == -1)
            return (void*)-1;
        // 字符数组末尾加 '\0'
        name_msg[str_len] = 0;
        // 输出格式化后的姓名和消息
        fputs(name_msg, stdout);
    }
    return NULL;
}

void error_handling( char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

四、Web服务器
1、Webserv_linux.c
/*
    程序名:webserv_linux.c
    功能:该程序为多线程Web服务器的服务器端,使用标准I/O函数,只是为了复习各种知识点
        基于HTTP协议,以浏览器(建议firefox)为客户端访问该服务器的html文件。
    作者:fouries
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

// 定义缓冲器 buf和 req_line的大小
#define BUF_SIZE 1024
#define SMALL_BUF 100

void* request_handler(void *arg);
void send_data(FILE* fp, char* ct, char* file_name);
char* content_type(char* file);
void send_error(FILE* fp);
void error_handling(char* message);

int main(int argc, char* argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int clnt_adr_size;
    char buf[BUF_SIZE];
    pthread_t t_id;
    if(argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if(listen(serv_sock, 20) == -1)
        error_handling("listen() error");
    
    while(1)
    {
        clnt_adr_size = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size);
        printf("Connection Request : %s:%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port));
        // 创建一个线程,从request_handler函数调用开始,在单独的执行流中运行。同时在调用request_handler函数时向其传递clnt_sock变量的地址值。
        pthread_create(&t_id, NULL, request_handler, &clnt_sock);
        // 摧毁一个线程, pthread_detach()不会引起堵塞
        pthread_detach(t_id);
    }
    close(serv_sock);
    return 0;
}

//  请求处理函数,请求处理线程的main函数
void* request_handler(void *arg)
{
    int clnt_sock = *((int*)arg);
    // 定义数组req_line用于保存请求行数据
    char req_line[SMALL_BUF];
    // 定义FILE*类型变量 clnt_read、clnt_write
    FILE* clnt_read;
    FILE* clnt_write;

    // 分别定义char型数组method、ct、file_name用于保存请求方法、内容类型、文件名
    char method[10];
    char ct[15];
    char file_name[30];

    // 以读的模式打开 clnt_sock
    clnt_read = fdopen(clnt_sock, "r");
    // 以写的模式打开 clnt_sock, dup函数创建一个新的文件描述符,该文件描述符和原有文件描述符指向相同的文件
    clnt_write = fdopen(dup(clnt_sock), "w");
    // 从clnt_read流中读取SMALL_BUF长度的字符串到 req_line数组
    fgets(req_line, SMALL_BUF, clnt_read);
    // 若 req_line中没有 "HTTP/"子串
    if(strstr(req_line, "HTTP/")==NULL)
    {
        send_error(clnt_write);
        fclose(clnt_read);
        fclose(clnt_write);
        return;
    }
    // strtok(char *str, const char *delim);以delim为分隔符分割str,返回被分解的第一个子字符串
    // 将req_line中的请求方法字符串保存于method数组中
    strcpy(method, strtok(req_line, " /"));
    // 将req_line中的文件名字符串保存于file_name数组中
    strcpy(file_name, strtok(NULL, " /"));
    // 将文件内容的类型保存于ct数组中
    strcpy(ct, content_type(file_name));
    // 判断 method是否为 GET
    if(strcmp(method, "GET")!=0)
    {
        send_error(clnt_write);
        fclose(clnt_read);
        fclose(clnt_write);
        return;
    }
    // 关闭clnt_read文件
    fclose(clnt_read);
    // 发送数据
    send_data(clnt_write, ct, file_name);
}

// 将头消息发送到 流fp
void send_data(FILE* fp, char* ct, char* file_name)
{
    // 状态行
    char protocol[] = "HTTP/1.0 200 OK\r\n";
    // 消息头
    char server[] = "Server:Linux Web Server \r\n";
    char cnt_len[] = "Content-length:2048\r\n";
    char cnt_type[SMALL_BUF];
    // 消息缓冲区
    char buf[BUF_SIZE];
    FILE* send_file;

    // 将"Content-type:%s\r\n\r\n"和 ct输入到 cnt_type
    sprintf(cnt_type, "Content-type:%s\r\n\r\n", ct);
    // 以读的模式打开 file_name, 并且赋值给 send_file
    send_file = fopen(file_name, "r");
    
    // 如果打开失败
    if(send_file == NULL)
    {
        send_error(fp);
        return;
    }

    /*传输头信息*/
    fputs(protocol, fp);
    fputs(server, fp);
    fputs(cnt_len, fp);
    fputs(cnt_type, fp);

    /*传输请求数据*/
    while(fgets(buf, BUF_SIZE, send_file)!=NULL)
    {
        fputs(buf, fp);
        fflush(fp);
    }
    // 刷新流fp的输出缓冲区
    fflush(fp);
    // 关闭流fp
    fclose(fp);
}

// 判断文件内容类型的函数
char* content_type(char* file)
{
    char extension[SMALL_BUF];
    char file_name[SMALL_BUF];
    // 得到 file的文件名,保存于 file_name中
    strcpy(file_name, file);
    strtok(file_name, ".");
    // 得到 file的扩展名保存于 extension中
    strcpy(extension, strtok(NULL, "."));
    // 判断file的扩展名是否为 html或者htm
    if(!strcmp(extension, "html")||!strcmp(extension, "htm"))
        return "text/html";
    else
        return "text/plain";
}

// http请求发生错误,发送错误信息函数
void send_error(FILE* fp)
{
    char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
    char server[] = "Server:Linux Web Server \r\n";
    char cnt_len[] = "Content-length:2048\r\n";
    char cnt_type[] = "Content-type:text/html\r\n\r\n";
    char content[] = "<html><head><title>NETWORK</title></head>"
        "<body><font size =+5><br>发生错误! 查看请求文件名和请求方式!"
        "</font></body></html>";

    fputs(protocol, fp);
    fputs(server, fp);
    fputs(cnt_len, fp);
    fputs(cnt_type, fp);
    fflush(fp);
}

// 错误处理函数
void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
2、index.html
<html>
<head><title>NETWORK</title></head>
<body><font size=+5>
TCP/IP Socket programming <br>
very interesting! 2021/2/18
</font></body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值