• socket编程实现tcp服务器_C/C++


    1. 需求分析

    实现一个回声服务器的C/S(客户端client/服务器server)程序,功能为客户端连接到服务器后,发送一串字符串,服务器接受信息后,返回对应字符串的大写形式给客户端显示。
    例如:

    客户端发送“this is a webserver example!",

    服务器返回"THIS IS A WEBSERVER EXAMPLE!"

    2. 项目实现

    2.1 服务器端程序echo_server.c

    #include <stdio.h>      //printf
    #include <stdlib.h>     //exit
    #include <unistd.h>     //read, write, close
    #include <sys/types.h>  //socket, bind, listen, accept
    #include <sys/socket.h> //socket, bind, listen, accept
    #include <string.h>     //strerror
    #include <ctype.h>      //inet_ntop
    #include <arpa/inet.h>  //inet_ntop
    #include <errno.h>      //strerror
    
    #define SERVER_PORT 666
    
    //出错处理
    void perror_exit(const char* des) {
        fprintf(stderr, "%s error, reason: %s\n", des, strerror(errno));
        exit(1);
    }
    
    int main(void){
    
        int sock;//代表信箱
        int ret;//作为bind和listen的返回值,用于处理出错信息
        struct sockaddr_in server_addr;
    
    
        //1.创建套嵌字(信箱)。成功:返回socket的文件描述符,失败:返回-1,设置errno
        sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock == -1) {
            perror_exit("create socket");
        }
    
        //2.清空服务器地址空间(标签),写上地址和端口号
        bzero(&server_addr, sizeof(server_addr));
    
        server_addr.sin_family = AF_INET;//选择协议族IPV4
        //inet_pton(AF_INET, "1.1.1.1", &server_addr.sin_addr.s_addr);//测试出错处理函数perror_exit
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址
        server_addr.sin_port = htons(SERVER_PORT);//绑定端口号
    
        //3. 实现标签贴到收信得信箱上
        ret = bind(sock, (struct sockaddr *)&server_addr,  sizeof(server_addr));
        if(ret == -1) {
            perror_exit("bind");
        }
    
        //4. 把信箱挂置到传达室,这样,就可以接收信件了(监听客户端)
        ret = listen(sock, 128);
        if(ret == -1) {
            perror_exit("listen");
        }
    
        //万事俱备,只等来信
        printf("等待客户端的连接\n");
    
        //5. 处理客户端请求
        int done =1;
        while(done){
            struct sockaddr_in client;
            int client_sock, len, i;
            char client_ip[64];
            char buf[256];
    
            socklen_t  client_addr_len;
            client_addr_len = sizeof(client);
            client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);
    
            //打印客服端IP地址和端口号
            printf("client ip: %s\t port : %d\n",
                     inet_ntop(AF_INET, &client.sin_addr.s_addr,client_ip,sizeof(client_ip)),
                     ntohs(client.sin_port));
            /*读取客户端发送的数据*/
            len = read(client_sock, buf, sizeof(buf)-1);
            buf[len] = '\0';
            printf("receive[%d]: %s\n", len, buf);
    
            //转换成大写
            for(i=0; i<len; i++){
                buf[i] = toupper(buf[i]);
            }
    
    
            len = write(client_sock, buf, len);
    
            printf("finished. len: %d\n", len);
            close(client_sock);
    
        }
        
        //6. 关闭连接
        close(sock);
        return 0;
    }
    

    2.2 客户端程序echo_client.c

    #include <stdio.h>      //printf
    #include <stdlib.h>     //exit
    #include <string.h>     //memset, strlen
    #include <unistd.h>     //read, write, close
    #include <sys/socket.h> //socket, connect
    #include <arpa/inet.h>  //inet_pton
    
    #define SERVER_PORT 666
    #define SERVER_IP  "127.0.0.1"
    int main(int argc, char *argv[]){//argc表示传入命令的个数,argv表示传入的具体信息
    
        int sockfd;
        char *message;
        struct sockaddr_in servaddr;
        int n;
        char buf[64];
    
        //异常处理
        if(argc != 2){
            fputs("Usage: ./echo_client message \n", stderr);
            exit(1);
        }
    
        message = argv[1];//传入的信息
        printf("message: %s\n", message);
    
        //1. 创建套嵌字
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
        memset(&servaddr, '\0', sizeof(struct sockaddr_in));//分配空间
    
        //定义地址IP和端口
        servaddr.sin_family = AF_INET;
        inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
        servaddr.sin_port = htons(SERVER_PORT);
    
        //2. 连接服务器
        connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    
        //3. 读写和服务器的交互信息
        write(sockfd, message, strlen(message));
    
        n = read(sockfd, buf, sizeof(buf)-1);
    
        if(n>0){
            buf[n]='\0';
            printf("receive: %s\n", buf);
        }else {
            perror("error!!!");
        }
    
        printf("finished.\n");
    
        //4. 关闭连接
        close(sockfd);
    
        return 0;
    }
    

    3. 程序运行方式

    我的echo_server.c程序在/share/echo_server文件夹下,echo_client.c程序在/share/echo_client文件夹下。
    必须先运行服务器程序,再运行客户端程序。顺序不能反!!

    3.1. 运行服务器程序

    • 首先,进入echo_server.c所在文件夹
      root@lxb-virtual-machine:/# cd /share/echo_server
    • 之后,编译程序
      root@lxb-virtual-machine:/share/echo_server# gcc echo_server.c -o echo_server
    • 最后运行程序
    • root@lxb-virtual-machine:/share/echo_server# ./echo_server

    全过程截图:
    image

    3.2. 运行客户端程序

    • 首先,进入echo_client.c所在文件夹
      root@lxb-virtual-machine:/# cd /share/echo_client
    • 之后,编译程序
      root@lxb-virtual-machine:/share/echo_client# gcc echo_client.c -o echo_client
    • 最后运行程序
    • root@lxb-virtual-machine:/share/echo_client# ./echo_client "this is a webserver example!"

    全过程截图:
    image

    4. 分析

    首先我们进行感性的分析,用来理解各个步骤的用意。之后我们需要对里面涉及到的函数进行具体的分析。

    4.1 李华写信模型

    我们在高中英语经常遇到的一道作文题就是“你是李华,请给国外的笔友Andy写信”,而我们网络通信也可以类比于“李华与国外笔友通信”的模型。这里我们将“李华”比作客户端,“国外笔友Andy”作为服务器端。

    4.1.1 Andy应该怎么做呢?

    为了使“李华同学”与“国外笔友”能够交流,首先需要统一语言,同时约定好寄信方式,邮局寄信还是电子邮件之类的,(这就是“socket套嵌字”)。之后Andy准备好一个信箱,之后找一张标签纸(server_addr),整理干净这张标签纸(bzero函数),往上面写上自己的地址和门牌号,一切准备好后,将贴好标签纸的信箱挂到外面(listen函数),这样大家都能给Andy寄信。最后Andy只需要时不时去看看信箱有没有信,有的话把信的内容读出来(read函数),之后再写封回信寄回去(write函数)。

    4.1.2 李华应该怎么做?

    李信作为写信人就比较简单了,首先还是使用统一的寄信方式,往信封上写上自己要寄的地址和门牌号,也就是Andy家的地址和门牌号,之后与Andy联系上(connect函数)。接下来就可以给Andy写信(write函数),读Andy的回信(read函数)。收到回信,不想再和Andy通信了,这时就把两个人的联系断开(close函数)。

    4.2 程序流程图

    image

    4.3 具体函数解析

    4.3.1 socket函数

    1. 所属头文件

      #include <sys/socket.h>

    2. 函数定义

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

    3. 参数含义

      • domain:
        AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
        AF_INET6 与上面类似,不过是来用IPv6的地址
        AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
      • type:
        SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
        SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
        SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
        SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
        SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
      • protocol:
        传0 表示使用默认协议。
      • 返回值
        成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno

    4.3.2 bind函数

    1. 所属头文件

      #include <sys/socket.h>

    2. 函数定义

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

    3. 参数含义

      • sockfd:
        socket文件描述符
      • addr:
        IP地址加端口号
      • addrlen:
        addr的长度
      • 返回值
        成功:返回0,失败:返回-1,设置errno

    4.3.3 listen函数

    1. 所属头文件

      #include <sys/socket.h>

    2. 函数定义

      int listen(int sockfd, int backlog);

    3. 参数含义

      • sockfd:
        socket文件描述符
      • backlog:
        在Linux 系统中,它是指排队等待建立3次握手队列长度
      • 返回值
        成功:返回0,失败:返回-1,设置errno

    4.3.4 accept函数

    1. 所属头文件

      #include <sys/socket.h>

    2. 函数定义

      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    3. 参数含义

      • sockfd:
        socket文件描述符
      • addr:
        IP地址加端口号
      • addrlen:
        addr的长度
      • 返回值
        成功:返回一个新的socket文件描述符,失败:返回-1,设置errno

    4.3.5 connect函数

    1. 所属头文件

      #include <sys/socket.h>

    2. 函数定义

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

    3. 参数含义

      • sockfd:
        socket文件描述符
      • addr:
        IP地址加端口号
      • addrlen:
        addr的长度
      • 返回值
        成功:返回一个新的socket文件描述符,失败:返回-1,设置errno

    4.3.6 出错处理函数

    1. 所属头文件

      #include <errno.h>
      #include <string.h>
      
    2. 函数定义

      char *strerror(int errnum);

    3. 参数含义

      • errnum:
        错误编号的值,一般取 errno 的值
      • 返回值
        错误原因

    5.感谢

    感谢bilibili的Martin老师的视频: C语言/C++服务器开发】小白实现第一个服务器入门项目 网络通信与Socket 编程详解&源码分享,本篇博客也是基于Martin老师这个视频所做的。

  • 相关阅读:
    #力扣:LCP 06. 拿硬币@FDDL
    VSCODE WIN x64 v1.69的python插件和jupyter插件的简单使用
    基于SSM的会议管理系统设计与实现
    【图论C++】链式前向星(图(树)的存储)
    SAP UI5 sap.ui.core.Element 的概要介绍
    怎么测量芯片IO的好坏
    协议-TCP协议-基础概念03-Keep live保活机制-TCP RST-TCP连接
    浅析Jetty与tomcat区别
    JAVA基础算法(11)----- 最大二叉树
    Packet Tracer - 配置 IPv6 的 EIGRP 基本设置
  • 原文地址:https://www.cnblogs.com/yunmeng-shi/p/16219550.html