TCP服务器

所需API如下:
(1)创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

  • socket()打开⼀个网络通讯端口,如果成功的话就像open()⼀样返回⼀个文件描述符
  • 应⽤程序可以像读写⽂件⼀样用read/write在网络上收发数据
  • 如果socket()调用出错则返回-1
  • 对于IPv4, family参数指定为AF_INET
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向字节流的传输协议
  • protocol参数指定为0即可

(2)绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调⽤bind绑定⼀个固定的网络地址和端口号
  • bind()成功返回0,失败返回-1
  • bind()的作用是将参数socket和address绑定在⼀起, 使socket这个用于网络通讯的文件描述符监听address所描述的地址和端口号
  • struct sockaddr *是⼀个通用指针类型,address参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数address_len指定结构体的⻓度

(3)开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

  • listen()声明socket处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太⼤(⼀般是5)
  • listen()成功返回0,失败返回-1

(4)接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

  • 三次握⼿完成后,服务器调⽤accept()接受连接
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
  • address是⼀个传出参数,accept()返回时传出客户端的地址和端⼝号
  • 如果给address参数传NULL,表示不关心客户端的地址;
  • address_len参数是⼀个传⼊传出参数(value-result argument),传⼊的是调⽤者提供的,缓冲区address的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际⻓度(有可能没有占满调⽤者提供的缓冲区)

(5)建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • 客户端需要调⽤connect()连接服务器
  • connect和bind的参数形式⼀致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址
  • connect()成功返回0,出错返回-1

server.c

///////////////////////////////////////////////////////////////
//服务器基本流程:
//1.从socket中读取数据(Request)
//2.根据 Request 计算生成 Response
//3.把 Response 写回客户端
//由于目前实现的是 echo_server, 计算生成 Response 步骤就省略了
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

void ProcessConnect(int new_sock){
    //完成一次连接的处理
    //需要循环的来处理客户端发送的数据
    while(1){
        //子进程
        //a)从客户端读取数据
        char buf[1024] = {0};
        ssize_t read_size = read(new_sock, buf, sizeof(buf) - 1);
        if(read_size < 0){
            perror("read");
            continue;
        }
        if(read_size == 0){
            //TCP中,如果read的返回值是0,说明对端关闭了连接
            printf("[client %d] disconnect!\n", new_sock);
            close(new_sock);
            return;
        }
        buf[read_size] = '\0';
        //b)根据请求计算响应(省略)
        printf("[client %d] %s\n", new_sock, buf);
        //c)把响应写回到客户端
        write(new_sock, buf, strlen(buf));
    }
}

//./server [ip] [port]
int main(int argc, char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip] [port]\n");
        return 1;
    }
    //1.创建 socket
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0){
        perror("socket");
        return 1;
    }
    //2.绑定端口号
    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    int ret = bind(listen_sock, (sockaddr*)&server, sizeof(server));
    if(ret < 0){
        perror("bind");
        return 1;
    }
    //3.使用 listen 允许服务器被客户端连接
    ret = listen(listen_sock, 5);
    if(ret < 0){
        perror("listen");
        return 1;
    }
    //4.服务器初始化完成,进入事件循环
    printf("Server Init OK!\n");
    while(1){
        //把小板凳上的顾客请到屋里吃饭
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
        if(new_sock < 0){
            perror("accept");
            continue;
        }
        printf("[client %d] connect!\n", new_sock);
        ProcessConnect(new_sock);
    }
    return 0;
}

描述

client.c

/////////////////////////////////////////////////////////////
//1.从标准输入读入字符串
//2.把读入的字符串发送给服务器
//3.尝试从服务器读取响应数据
//4.把响应结果打印到标准输出上
////////////////////////////////////////////////////////////
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

int main(int argc, char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip] [port]\n");
        return 1;
    }
    //1.创建 socket
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0){
        perror("socket");
        return 1;
    }
    //2.获取连接
    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    server_addr.sin_port = htons(atoi(argv[2]));
    int ret = connect(fd, (sockaddr*)&server_addr, sizeof(server_addr));
    if(ret < 0){
        perror("connect");
        return 1;
    }
    //3.进入循环
    while(1){
        //a)从标准输入读入字符串
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if(read_size < 0){
            perror("read");
            return 1;
        }
        if(read_size == 0){
            printf("read done!\n");
            return 0;
        }
        buf[read_size] = '\0';
        //b)把读入的字符串发送给服务器
        write(fd, buf, strlen(buf));
        //c)尝试从服务器读取响应数据
        char buf_resp[1024] = {0};
        read_size = read(fd, buf_resp, sizeof(buf_resp) - 1);
        if(read_size < 0){
            perror("read");
            return 1;
        }
        if(read_size == 0){
            printf("server close socket!\n");
            return 0;
        }
        buf_resp[read_size] = '\0';
        //d)把响应结果打印到标准输出上
        printf("server resp: %s\n", buf_resp);
    }
    return 0;
}

由于客户端不需要固定的端⼝号,因此不必调⽤bind(),客户端的端⼝号由内核⾃动分配
注意:

  • 客户端不是不允许调⽤bind(),只是没有必要调⽤bind()固定⼀个端⼝号。否则如果在同⼀台机器上启动多个客户端, 就会出现端⼝号被占⽤导致不能正确建⽴连接;
  • 服务器也不是必须调⽤bind(),但如果服务器不调⽤bind(),内核会⾃动给服务器分配监听端⼝,每次启动服务器时端⼝号都不⼀样,客户端要连接服务器就会遇到⿇烦
    描述

若再启动⼀个客户端,尝试连接服务器,发现第⼆个客户端不能正确的和服务器进⾏通信。
分析原因:是因为我们accept了⼀个请求之后就在⼀直while循环尝试read,没有继续调⽤到accept,导致不能接受新的请求。
改进方法有两个,可以采用多进程和多线程版本。

server_fork.c

///////////////////////////////////////////////////////////////
//服务器基本流程:
//1.从socket中读取数据(Request)
//2.根据 Request 计算生成 Response
//3.把 Response 写回客户端
//由于目前实现的是 echo_server, 计算生成 Response 步骤就省略了
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

void ProcessConnect(int new_sock, sockaddr_in* peer){
    ////////////////////////////////////////////////////////////
    //在这个函数中进行创建子进程来处理客户端的请求
    ////////////////////////////////////////////////////////////
    //完成一次连接的处理
    //需要循环的来处理客户端发送的数据
    while(1){
        int ret = fork();
        if(ret < 0){
            perror("fork");
            return;
        }
        if(ret > 0){
            //父进程
            //此处一定要考虑到僵尸进程的问题
            //此处使用wait和waitpid都是不行的
            //比较简单的方案就是忽略SIGCHLD信号
            signal(SIGCHLD, SIG_IGN);
            //此处还需要注意!!!
            //文件描述符需要父子进程都关闭
            close(new_sock);
            return;
        }
        //子进程
        //a)从客户端读取数据
        char buf[1024] = {0};
        ssize_t read_size = read(new_sock, buf, sizeof(buf) - 1);
        if(read_size < 0){
            perror("read");
            continue;
        }
        if(read_size == 0){
            //TCP中,如果read的返回值是0,说明对端关闭了连接
            printf("[client %s:%d] disconnect!\n", inet_ntoa(peer->sin_addr), ntohs(peer->sin_port));
            close(new_sock);
            //此处需要注意!!!
            //不能直接让函数返回,而是让子进程直接退出
            //如果是函数返回,子进程接下来也会尝试进行 accept
            //这样的动作是没有必要的,父进程已经负责了 accept
            //子进程只要做好自己的事情,把对应的客户端服务好就可以了
            exit(0);
        }
        buf[read_size] = '\0';
        //b)根据请求计算响应(省略)
        printf("[client %s:%d] %s\n", inet_ntoa(peer->sin_addr), ntohs(peer->sin_port),  buf);
        //c)把响应写回到客户端
        write(new_sock, buf, strlen(buf));
    }
}

//./server [ip] [port]
int main(int argc, char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip] [port]\n");
        return 1;
    }
    //1.创建 socket
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0){
        perror("socket");
        return 1;
    }
    //2.绑定端口号
    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    int ret = bind(listen_sock, (sockaddr*)&server, sizeof(server));
    if(ret < 0){
        perror("bind");
        return 1;
    }
    //3.使用 listen 允许服务器被客户端连接
    ret = listen(listen_sock, 5);
    if(ret < 0){
        perror("listen");
        return 1;
    }
    //4.服务器初始化完成,进入事件循环
    printf("Server Init OK!\n");
    while(1){
        //把小板凳上的顾客请到屋里吃饭
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
        if(new_sock < 0){
            perror("accept");
            continue;
        }
        printf("[client %s:%d] connect!\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
        ProcessConnect(new_sock, &peer);
    }
    return 0;
}

客户端1
客户端2
服务器端

server_thread.c

///////////////////////////////////////////////////////////////
//服务器基本流程:
//1.从socket中读取数据(Request)
//2.根据 Request 计算生成 Response
//3.把 Response 写回客户端
//由于目前实现的是 echo_server, 计算生成 Response 步骤就省略了
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

void* ThreadEntry(void* arg){
    int64_t new_sock = (int64_t)arg;
    while(1){
        //a)从客户端读取数据
        char buf[1024] = {0};
        ssize_t read_size = read(new_sock, buf, sizeof(buf) - 1);
        if(read_size < 0){
            perror("read");
            continue;
        }
        if(read_size == 0){
            //TCP中,如果read的返回值是0,说明对端关闭了连接
            printf("[client %lu] disconnect!\n", new_sock);
            close(new_sock);
            return NULL;
        }
        buf[read_size] = '\0';
        //b)根据请求计算响应(省略)
        printf("[client %lu] %s\n", new_sock, buf);
        //c)把响应写回到客户端
        write(new_sock, buf, strlen(buf));
    }
    return NULL;
}

void ProcessConnect(int64_t new_sock){
    //////////////////////////////////////////////////////
    //此处需要创建线程来完成和客户端的交互
    //////////////////////////////////////////////////////
    //完成一次连接的处理
    //需要循环的来处理客户端发送的数据
    pthread_t tid;
    pthread_create(&tid, NULL, ThreadEntry, (void*)new_sock);
    pthread_detach(tid);
}

//./server [ip] [port]
int main(int argc, char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip] [port]\n");
        return 1;
    }
    //1.创建 socket
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0){
        perror("socket");
        return 1;
    }
    //2.绑定端口号
    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    int ret = bind(listen_sock, (sockaddr*)&server, sizeof(server));
    if(ret < 0){
        perror("bind");
        return 1;
    }
    //3.使用 listen 允许服务器被客户端连接
    ret = listen(listen_sock, 5);
    if(ret < 0){
        perror("listen");
        return 1;
    }
    //4.服务器初始化完成,进入事件循环
    printf("Server Init OK!\n");
    while(1){
        //把小板凳上的顾客请到屋里吃饭
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
        if(new_sock < 0){
            perror("accept");
            continue;
        }
        printf("[client %d] connect!\n", new_sock);
        ProcessConnect(new_sock);
    }
    return 0;
}

客户端1
客户端2
服务器端

>详细代码请参考Git<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值