基于TCP协议的服务器

本博客已弃用,当时存在一些小细节错误后期也不再修改了

欢迎来我的新博客

先复习一下UDP协议:UDP协议为"user datagram protocol",用户数据报协议,对于UDP来说,其最小传输单元是数据报,并且是只管发送,不用建立连接,尽最大可能的发送数据,不保证可靠性。由于其不能拆分、合并应用层传递下来的报文,导致不能灵活的控制接收的次数与数量,且单个数据报最大只能是64k,但是由于不用建立连接,使其非常轻便、快速,且占用资源少。

TCP协议为"Transmission Control Protocol",传输控制协议。名副其实,TCP协议对数据传输的每一个细节几乎都控制到了,当然,这也导致TCP协议相当复杂,很多细节,如最开始的连接过程的三次握手到断开连接的四次挥手、超时重传机制、滑动窗口、流量控制、拥塞控制、延迟应答等等,都是TCP的非常重要的机制,在此我们就不讨论这些了,讲这方面的书籍与资源实在太多了。

因为既要保证可靠性,又要使性能尽可能的高,所以TCP的相对UDP较复杂是必然的。与UDP相反,TCP面向字节流,收发信息用系统调用write与read即可完成,既然面向字节流,那么无疑是可以自己灵活的控制读写次数,对于对方发来的100字节的数据,我可以分100次读,也可以一次读,同时在write数据时,如果数据太长,TCP协议会给你做好分包工作。当然,其传输的可靠性、重传机制以及高效性都非常不错,不过本篇我们的重点不是讲通信细节,而是服务器的实现,在此就不一一去说了。但是值得注意的是,这些理论是相当重要的,这是一个程序员必须保证的基本功,对于TCP协议应该清楚的知道每个api对应了哪些通信细节与机制。


TCP协议的常用接口

// 创建 socket ⽂文件描述符 ( 客户端 + 服务器)

int socket(int domain, int type, int protocol);

// 绑定端⼝口号 ( 服务器)      

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket ( 服务器)

int listen(int socket, int backlog);

// 接收请求 (服务器)

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建⽴立连接 (客户端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

我们仍然写一个echo聊天服务器,首先是最简单的单进程阻塞版本

#include 
#include 
#include 
#include 
#include 
#include 
#include 
//tcp_server ip port

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s is ip port",argv[0]);
        return 1;
    }
    
    int listen_sock = socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock < 0)
    {
        perror("listen_socket");
        return 2;
    }
    printf("listen_socket:%d\n",listen_sock);

    //填充本地信息
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));//端口号
    local.sin_addr.s_addr = inet_addr(argv[1]);//IP地址

    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        return 3;
    }

    if(listen(listen_sock,5) < 0)
    {
        perror("listen");
        return 4;
    }

    for(;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
        if(new_sock < 0)
        {
            perror("accept");
            continue;
        }

        printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
        fflush(stdout);
        char buf[128];
        while(1)
        {
            ssize_t s = read(new_sock,buf,sizeof(buf)-1);
            if(s > 0)
            {
                buf[s] = 0;
                printf("client: %s\n",buf);
                write(new_sock,buf,strlen(buf));
            }
            else if(s == 0)//连接关闭
            {
                printf("quit!\n");
                break;
            }
            else
            {
                perror("read");
                break;
            }
        }
        close(new_sock);
    }

}

这样的单进程阻塞版本只能一个客户端和服务器通信,另外一个客户端想连接服务器是连不上来的。首先accept函数是阻塞的,如果没有客户端连接监听套接字那么accept会一直阻塞,知道有一个客户端连接到服务器,这时accept返回,代码走到收发数据的循环,知道第一个客户端断开,跳出收发循环前,服务器不会再走到accpet函数了,这就导致第二个客户端在第一个客户端退出前是无法连进来的,这显然是不合理的。TCP协议有面向连接的特点,因此这个问题是一定无法避免的。有三种常见的方案。在此先讲两种,第三种下次专门讲。


多进程版本

通过fork子进程,让子进程去进行真正的数据收发工作,而父进程则不断在接收各个客户端的连接。这里要处理的一个问题就是子进程死亡后如何处理,首先简单在父进程值wait()是绝对不行的,因为wait是阻塞的,这样又得等到子进程死亡后父进程才能继续让其他客户端接入。

最常用的方法就是在服务器中注册信号SIGCHLD为SIG_IGN,这是高并发多进程服务器的常用方法,在linux下,忽略SIGCHLD信号后,内核会子进程死亡后的僵尸进程交给init进程处理,除此之外还有一个理论上来说应用范围更广的方法:

        pid_t id = fork();
        if(id == 0)    //child
        {
            if(fork() > 0)
            {
                exit(0);
            }
            close(listen_sock);
            serviceIO(new_sock);
            exit(0);
        }
        else           //father
        {       
            //close(new_sock);
            waitpid(id,NULL,0);         //绝对不会阻塞了,因为子进程立即退出了,父进程立马可以回收它
        }

在子进程中创建孙进程,在子进程让子进程自己立即退出,孙进程作为服务进程来收发数据,收发结束后退出。在此过程中,子进程立马就退出了,在父进程的执行流中waitpid马上就可以回收子进程并返回,绝对不会阻塞,而孙进程由于子进程的退出,自身成了孤儿进程,交由init进程管理其死亡后的处理。这个方法需要注意的是,一定要serviceIO函数中关闭用于数据传输的套接字,否则父进程的可用文件描述符会越来越少,会严重影响其性能瓶颈。

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

//tcp_server ip port
//多进程
void serviceIO(int sock)
{
    
    char buf[128];
    while(1)
    {
        ssize_t s = read(sock,buf,sizeof(buf)-1);
        if(s > 0)
        {
            buf[s] = 0;
            printf("client: %s\n",buf);
            write(sock,buf,strlen(buf));
        }
        else if(s == 0)//连接关闭
        {
            printf("quit!\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(sock);
}

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s is ip port",argv[0]);
        return 1;
    }
    
    int listen_sock = socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock < 0)
    {
        perror("listen_socket");
        return 2;
    }
    printf("listen_socket:%d\n",listen_sock);

    //填充本地信息
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));//端口号
    local.sin_addr.s_addr = inet_addr(argv[1]);//IP地址

    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        return 3;
    }

    if(listen(listen_sock,5) < 0)
    {
        perror("listen");
        return 4;
    }

    while(1)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);   //阻塞的
        if(new_sock < 0)
        {
            perror("accept");
            continue;
        }
         
        printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));

        pid_t id = fork();
        if(id == 0)    //child
        {
            if(fork() > 0)
            {
                exit(0);
            }
            close(listen_sock);
            serviceIO(new_sock);
            exit(0);
        }
        else           //father
        {       
            //close(new_sock);
            waitpid(id,NULL,0);         //绝对不会阻塞了,因为子进程立即退出了,父进程立马可以回收它
        }
    }

}

 多线程版本

多线程版本通过创建线程,使各个线程与客户端进程数据传输,而主线程则不断accept各个客户端的连接,值得注意的是线程要设为detach状态,是其死亡后自会释放资源,在线程的处理函数中也一定要关闭用于数据传输的套接字,否则也会影响主线程。

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

//多线程
//tcp_server ip port

void serviceIO(int sock)
{
    
    char buf[128];
    while(1)
    {
        ssize_t s = read(sock,buf,sizeof(buf)-1);
        if(s > 0)
        {
            buf[s] = 0;
            printf("client: %s\n",buf);
            write(sock,buf,strlen(buf));
        }
        else if(s == 0)//连接关闭
        {
            printf("quit!\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(sock);
}

void* service(void* arg)
{
    int sock = (int)arg;
    serviceIO(sock);
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s is ip port",argv[0]);
        return 1;
    }
    
    int listen_sock = socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock < 0)
    {
        perror("listen_socket");
        return 2;
    }
    printf("listen_socket:%d\n",listen_sock);

    //填充本地信息
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));//端口号
    local.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址

    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        return 3;
    }

    if(listen(listen_sock,5) < 0)
    {
        perror("listen");
        return 4;
    }

    while(1)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);   //阻塞的
        if(new_sock < 0)
        {
            perror("accept");
            continue;
        }
         
        printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));

        pthread_t id;
        pthread_create(&id,NULL,service,(void *)new_sock);
        pthread_detach(id);
    }

}

客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
//tcp_client ip port

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s is server_ip server_port",argv[0]);
        return 1;
    }
    
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        return 2;
    }
    printf("socket:%d\n",sock);

    //填充本地信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));//端口号
    server.sin_addr.s_addr = inet_addr(argv[1]);//IP地址

    if(connect(sock, (struct sockaddr*)&server,sizeof(server)) < 0)
    {
        perror("connect");
        return 3;
    }

    char buf[128];
    while(1)
    {
        printf("Please Enter:");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s > 0)
        {
            buf[s] = 0;
            write(sock,buf,strlen(buf));
            read(sock,buf,sizeof(buf)-1);
            printf("server echo: %s\n",buf);
        }
    }
    
}

源码

你可能感兴趣的:(Linux)