普通版 / 多进程 / 多线程 --- TCP服务器实现原理详解

UDP服务器请查看: 简单的UDP服务器实现

  • 代码注释很详细,不了解的可以看代码,基本一半以上都是注释。

tcp_server.c (普通版)

#include<stdio.h>
#include<sys/socket.h>     
#include<netinet/in.h>
#include<errno.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<arpa/inet.h>
/*
1、创建socket
2、绑定端口号
3、把socket转换成被动模式(listen)
4、循环进行accept
5、从accept返回的new_fd中读取客户端请求
6、读取到数据后,处理数据
7、把处理后的结果返回给客户端
*/
#define _PORT_ 8080
#define _BACKLOG_ 10
int main()
{
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  //socket()打开一个网络通讯端口,成功返回一个文件描述符
  //应用程序可以像读写文件一样用read、write读写文件
  //socket调用出错返回-1
  //对于IPV4,family指定为AF_INET
  //对于TCP协议,type为SOCK_STREAM,表示面向数据流的协议
   //protocal参数指定为0即可

  if(sock < 0)   //失败返回-1
  {
     printf("create socket weeor, error is:%d, errstring is :%s\n", errno, strerror(errno));
  }

  struct sockaddr_in server_socket;
  struct sockaddr_in client_socket;
  bzero(&server_socket, sizeof(server_socket));
  server_socket.sin_family = AF_INET;
  server_socket.sin_addr.s_addr = htonl(INADDR_ANY);
  server_socket.sin_port = htons(_PORT_);

  if(bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in) < 0))

  /*
  服务器端的网络地址和端口号一般不变,客户端知道了服务器IP和端口号后就可以向服务器发起连接,服务器需要调用一个bind绑定一个固定的网络IP和端口号

  成功返回0,失败返回-1
  bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号,

  struct sockaddr是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,
  而他们的长度各不相同,所以需要第三个参数addrlen指定结构体长度

     bzero(&server_socket, sizeof(server_socket));
     server_socket.sin_family = AF_INET;
     server_socket.sin_addr.s_addr = htonl(INADDR_ANY);
     server_socket.sin_port = htons(_PORT_);
1、将整个结构体清零
2、设置地址类型为AF_INET
3、INADDR_ANY表示任意的IP地址,因为服务器可能有多个网卡,每个网卡可能绑定多个IP,这样设置,可以在所有的IP上监听,
   直到与某个客户端建立了连接,才确定到底用的那个IP
4、_PORT_ 端口号
  */
  { //失败处理
    printf("Bind error, error code is :%d, error string is :%s\n", errno, strerror(errno));
    close(sock);
    return 1;
  }


  if(listen(sock, _BACKLOG_) < 0)

  /*
  listen()声明sock_fd处于监听状态,最多允许有_BACKLOG_个客户端处于连接等待状态,如果接收到更多的连接就忽略,(注意这里设置一般不大,为5)
  listen()成功返回0,失败返回-1
  */

  {   //处理失败
    printf("listen error\n");
    return 2;
  }

  printf("bind and listen successed!\n");

  for(;;)
  {
    socklen_t len = 0;
    int client_sock = accept(sock, (struct sockaddr *)&client_sock, &len);

/*
三次握手完成后,服务器调用accept接受数据,
如果服务器调用accept()还没有客户端的连接请求,就阻塞等待,直到有客户端连接。
addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
如果给addr参数传NULL,表示不关心客户端地址。
addrlen是一个传入,传出参数,传入是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长的(有可能没有占满调用者提供的缓冲区)

若成功则为非负描述符,若出错则为-1
如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与客户端的TCP连接。一个服务器通常仅仅创建一个监听套接字,它在该服务器生命周期内一直存在。内核为每个由服务器进程接受的客户端连接创建一个已连接套接字。当服务器完成对某个给定的客户端的服务器时,相应的已连接套接字就被关闭。
*/
    if(client_sock < 0)
    {
       printf("accept error\n");
       close(sock);
       return 3;
    }
    char buf_ip[INET_ADDRSTRLEN];
    memset(buf_ip, '\0', sizeof(buf_ip));
    inet_ntop(AF_INET, &client_socket.sin_addr, buf_ip, sizeof(buf_ip));

    printf("get connect,ip is :%s , port is :%d \n", buf_ip, ntohs(client_socket.sin_port));

    while(1)
    {
      char buf[1024];
      memset(buf, '\0', sizeof(buf));

      read(client_sock, buf, sizeof(buf));
      printf("client :# %s\n", buf);

      printf("server :$ ");

      memset(buf, '\0', sizeof(buf));
      fgets(buf, sizeof(buf), stdin);
      buf[strlen(buf)-1] = '\0';
      write(client_sock, buf, strlen(buf) + 1);
      printf("please wait ……");
    }
  }
}

tcp_client.c (普通版)

#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<errno.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<arpa/inet.h>
/*
1、创建socket
2、和服务器建立连接
3、给服务器发送数据
4、从服务器读取返回的结果
5、和服务器断开连接
*/
//客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配
#define SERVER_PORT_ 8080
#define SERVER_IP "118.24.7681"

int main(int argc, char *argv[])
{
  if(argc != 2)
  {
    printf("Usage : client IP\n");
    return 1;
  }

  char *str = argv[1];
  char buf[1024];
  memset(buf, '\0', sizeof(buf));

  struct sockaddr_in server_sock;
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  bzero(&server_sock, sizeof(server_sock));
  server_sock.sin_family = AF_INET;
  inet_pton(AF_INET, SERVER_IP, &server_sock.sin_addr);
  server_sock.sin_port = htons(SERVER_PORT_);

  int ret = connect(sock, (struct sockaddr *)&server_sock, sizeof(server_sock));
/*
调用connect连接服务器
connect和bind的参数形式一致,区别在于bind参数是自己的地址,connect参数是对方地址。
成功返回0
失败返回-1
*/
  if(ret < 0)
  {
    printf("connect failed !!!\n");
    return 1;
  }
  printf("connect successed ……\n");

  while(1)
  {
    printf("client:# ");
    fgets(buf, sizeof(buf), stdin);
    buf[strlen(buf) - 1] = '\0';
    write(sock, buf, sizeof(buf));

    if(strncasecmp(buf, "quit", 4) == 0)
    {
      printf("quit!\n");
      break;
    }
    printf("wait ---\n");
    read(sock, buf, sizeof(buf));
    printf("server $: %s", buf);
  }
  close(sock);
  return 0;
}
  • 普通TCP存在一个问题就是客户端和服务器只能一对一绑定,多个客户端不能同时访问一个服务器,所以便有了多进程版的服务器。

tcp_server.c (多进程版)

#include <stdio.h>
#include <sys/types.h>   //
#include <sys/socket.h>  // socket()  bind()
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>


static void usage(const char* proc)
{
    printf("use help : %s [local_ip] [local_port]\n", proc);
}

    // 创建并绑定一个socket
int startup(const char* _ip, int _port)
{
    // 创建一个socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)  //创建失败处理
    {
        perror("----sock----fail\n");
        exit(-1);
    }

    // 绑定一个本地socket
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = inet_addr(_ip);
    local.sin_port = htons(_port);
    //绑定端口号
    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) != 0)
    {
        perror("---bind---fail\n");
        close(sock);
        exit(-2);
    }
    //监听
    if(listen(sock, 5) != 0)
    {
        perror("----listen----fail\n");
        close(sock);
        exit(-1);
    }

    return sock;        
}

int main(int argc, char**argv)
{
    if(argc != 3)
    {
        usage(argv[0]);
        return -1;
    }

    // 获取一个local socket
    int listen_sock = startup(argv[1], atoi(argv[2]));
    struct sockaddr_in client;
    socklen_t len = sizeof(client);

    char buf[1024];
    while(1)
    {
        int new_sock = accept(listen_sock,(struct sockaddr*)&client, &len);
        if(new_sock < 0)
        {
            perror("----accept----fail\n");
            close(listen_sock);
            exit(-5);
        }
        pid_t id = fork();
        if(id == 0)
        {
            //可以再这里fork 然后退出子进程,这样负责监听的进程PID就不会一直变
            close(listen_sock); 
            // 子进程已经获得 sockfd, 不需要listen sock所以关闭
            while(1)
            {
                ssize_t s = read(new_sock,buf, sizeof(buf)-1);
                if(s > 0 )
                {
                    buf[s] = 0;
                    printf("client say## %s \n", buf);
                    write(new_sock, buf, strlen(buf));
                }
                else if( s == 0)
                {
                    printf("client quit. \n");
                    break;
                }
                else
                    break;
            }
            exit(0);

        }
        else if (id > 0)
        {
            // 父进程
            // 在父进程中fork退出的话,会导致监听进程的pid一直在变,所以我们也可以再子进程中fork然后退出
            // 此时子进程已经获取 sockfd, 父进程只需要监听sockfd,
            //又因为每次fork都会赋值描述符,为了节省资源,关闭new_sock
            if(fork() > 0)
            {
                exit(0);
            }
            // 父进程退出,此时第二次fork出的子进程 成为孤儿进程,此后它产生的子进程退出时由系统来回收资源
        }
    }

    return 0;
}

在server.c中,我们先fock了两个进程父进程和子进程。父进程的工作是不断地返回accept处拿new_sock,子进程的工作是不断地去处理new_sock。也就是说父进程只管拿,子进程只管处理。所以父进程关闭不需要的套接字new_sock,而只留下listen_sock ; 子进程关闭不需要的listen_sock,而留下new_sock.

为什么还在子进程中再次fock创建一个孙子进程?这也正是这段代码的巧妙之处。
通过之前的学习我们知道,子进程退出后变成僵尸进程,直到父进程调用wait或者waitpid来对子进程资源进行回收。所以在第一次fork完成后,我们父进程会调用waitpid等待函数去等待子进程结束,但是它目前是阻塞式等待。为了解决阻塞问题,我们在子进程中又创建了一个孙子进程,并且让子进程退出,让孙子进程去完成子进程的工作。而对于孙子进程来说,子进程的退出使孙子进程变成孤儿进程,被1号进程Init收养,若孙子进程退出也是由Init进程回收它,与父进程中的waitpid无关。这样当子进程退出时,父进程的waitpid对子进程资源进行了回收,并且回收后因为子进程已经不存在了,waitpid也失去了它的作用,就不会阻塞了。这就是这段代码的巧妙之处。

多线程版也是为了支持多连接。类似多进程。

tcp_server.c (多线程版)

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>

static void Usage(const char* proc)
{
    printf("Usage:%s[local_ip][local_port]\n",proc);
}


int StartUp(const char* ip,int port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
    if(sock < 0){
        perror("socket");
        exit(2);
    }   

    struct sockaddr_in local;//IPV4
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);

    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
        perror("bind");
        exit(3);
    }

    if(listen(sock,10) < 0){//listen
        perror("listen");
        exit(4);
    }

    return sock;//return an "listen_sock";
}

void* request(void* arg)
{
    int new_sock = (int)arg;
    printf("get a new client\n");
    while(1)
    {//first read,then write
     char buf[1024];
     ssize_t s = read(new_sock,buf,sizeof(buf)-1);

     if(s > 0){//read successful
         buf[s] = '\0';
         printf("client#:%s\n",buf);
         write(new_sock,buf,strlen(buf));//server hui xian
     }
     else if(s == 0){//read to file's end
         printf("client exit\n");
         break;
     }
     else{//read fail
         perror("read");
         break;
     }
    }


}
int main(int argc,char* argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        return 1;
    }

    int listen_sock = StartUp(argv[1],atoi(argv[2]));//get listen_sock

    while(1)
    {
        struct sockaddr_in client;//get sender's information
        socklen_t len = sizeof(client);

        //get real socket that we will operator
        int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
        if(new_sock < 0){
            perror("accept");//if fail,again listen
            continue;
        }

        pthread_t id;
        pthread_create(&id,NULL,&request,(void*)new_sock);
        pthread_detach(id);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_37925202/article/details/81036585