4

socket编程实现tcp服务器_C/C++ - 云梦士

 2 years ago
source link: https://www.cnblogs.com/yunmeng-shi/p/16219550.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

全过程截图:

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!"

全过程截图:

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

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 程序流程图

4.3 具体函数解析

4.3.1 socket函数

  1. 所属头文件

    #include <sys/socket.h>

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

    • 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);

    • 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);

    • 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);

    • 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);

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

4.3.6 出错处理函数

  1. 所属头文件

    #include <errno.h>
    #include <string.h>
    
  2. char *strerror(int errnum);

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK