工学1号馆

home

一个简单的基于 TCP/IP 的 echo 服务器

Wu Yudong    November 29, 2016     Linux/Unix   514   

本文将实现一个echo(回传)服务器的程序:

(1)客户从标准输入读入一行文本,并写个服务器;

(2)服务器从网络输入读入这行文本,并回传给客户;

(3)客户从网络输入读入这行回传文本,并显示在标准输出上

这个例子是一个尽管简单但是有效的网络应用的例子,实现任何客户/服务器网络应用所需的所有基本步骤都可以通过本例子阐明。如果你想把本例扩充成自己的应用程序,只需修改服务器对来自客户的输入的处理过程。

TCP/IP 回传服务器

这个程序描述了一个简单的 TCP/IP 服务器, 它将接受一个来自客户端的应用的连接,接收一行文本,将这行文本回传到客户端并关闭连接。

这个程序诠释了一个经典的 TCP/IP 服务端程序的处理过程,总体而言,分下面几步:

  • 调用 socket()来创建一个连接
  • 将IP地址设置成 INADDR_ANY(服务器将监听任何IP地址)并且使用任意你希望使用的端口,来创建并初始化一个 socket 地址结构.
  • 调用 bind() 函数来将socket地址绑定到套接字.
  • 调用 listen() 函数表明这是一个主动的套接字,我们想要接收传入的请求而不是向外的
  • 进入一个循环,在这我们将:
    1. 调用 accept() 来等待一个连接到来
    2. 为新的连接请求提供服务
    3. close() 连接,接着用循环等待另一个新的连接

服务端进入无限循环,它自身没有没有机制关闭, 我们需要使用类似kill的命令来终止它。

接下来看一下代码

helper.h

#include <unistd.h>             /*  ssize_t类型  */
#define LISTENQ        (1024)   /*  指定系统内核允许在这个监听描述符上排队的最大客户连接数   */
ssize_t Readline(int fd, void *vptr, size_t maxlen);
ssize_t Writeline(int fc, const void *vptr, size_t maxlen);

helper.c

#include "helper.h"
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>

/*  从一个套接字读取一行文本  */
ssize_t Readline(int sockd, void *vptr, size_t maxlen) {
    ssize_t n, rc;
    char    c, *buffer;
    buffer = vptr;
    for ( n = 1; n < maxlen; n++ ) {
        if ( (rc = read(sockd, &c, 1)) == 1 ) {
            *buffer++ = c;
            if ( c == '\n' )
            break;
        } else if ( rc == 0 ) {
            if ( n == 1 )
                return 0;
            else
                break;
        } else {
            if ( errno == EINTR )
                continue;
            return -1;
        }
    }
    *buffer = 0;
    return n;
}

/*  向一个套接字写入一行文本  */
ssize_t Writeline(int sockd, const void *vptr, size_t n) {
    size_t      nleft;
    ssize_t     nwritten;
    const char *buffer;

    buffer = vptr;
    nleft  = n;
    while ( nleft > 0 ) {
        if ( (nwritten = write(sockd, buffer, nleft)) <= 0 ) {
            if ( errno == EINTR )
                nwritten = 0;
            else
                return -1;
        }
        nleft  -= nwritten;
        buffer += nwritten;
    }
    return n;
}

echoserv.c

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "helper.h"

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

#define ECHO_PORT          (2002)
#define MAX_LINE           (1000)

int main(int argc, char *argv[])
{
    int list_s;                    /*  监听 socket      */
    int conn_s;                    /*  连接 socket      */
    short int port;                /*  端口号           */
    struct sockaddr_in servaddr;   /*  socket地址结构   */
    char buffer[MAX_LINE];        /*  字符缓冲          */
    char *endptr;                

    /*  从命令行获取端口号,如果没有参数提供就使用默认的端口号  */
    if (argc == 2) {
        port = strtol(argv[1], &endptr, 0);
        if (*endptr) {
            fprintf(stderr, "ECHOSERV: Invalid port number.\n");
            exit(EXIT_FAILURE);
        }
    } else if (argc < 2) {
        port = ECHO_PORT;
    } else {
        fprintf(stderr, "ECHOSERV: Invalid arguments.\n");
        exit(EXIT_FAILURE);
    }

    /*  创建监听 socket  */
    if ((list_s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "ECHOSERV: Error creating listening socket.\n");
        exit(EXIT_FAILURE);
    }

    /*  将套接字地址结构中的所有字节都清0,并为其赋值  */
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(port);

    /*  绑定我们的套接字地址到监听套接字,并调用 listen()  */
    if (bind(list_s, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        fprintf(stderr, "ECHOSERV: Error calling bind()\n");
        exit(EXIT_FAILURE);
    }
    if (listen(list_s, LISTENQ) < 0) {
        fprintf(stderr, "ECHOSERV: Error calling listen()\n");
        exit(EXIT_FAILURE);
    }

    /*  进入一个无限循环来响应客户端请求并回传输入 */
    while (1) {

        /*  等待连接,然后 accept()   */
        if ((conn_s = accept(list_s, NULL, NULL)) < 0) {
            fprintf(stderr, "ECHOSERV: Error calling accept()\n");
            exit(EXIT_FAILURE);
        }

        /*  收到从已连接的套接字输入的一个文本行,然后简单地写入相同的套接字.  */
        Readline(conn_s, buffer, MAX_LINE - 1);
        Writeline(conn_s, buffer, strlen(buffer));

        /*  关闭已连接的 socket  */
        if (close(conn_s) < 0) {
            fprintf(stderr, "ECHOSERV: Error calling close()\n");
            exit(EXIT_FAILURE);
        }
    }
}

接下来看看TCP/IP 客户端,它将用来连接到回传服务器并回传一个字符串

这个程序诠释了一个经典的 TCP/IP 客户端程序的处理过程,总体而言,分下面几步:

  • 调用 socket() 来创建一个套接字
  • 使用地址族(这里是 AF_INET)创建和初始化一个套接字地址结构,服务器的远程 IP 地址和服务器监听的端口
  • 通过调用connect() 主动连接到服务器
  • 与服务器通信, 在这种情况下允许服务器执行主动关闭

代码如下:

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "helper.h"

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

#define MAX_LINE           (1000)

int ParseCmdLine(int argc, char *argv[], char **szAddress, char **szPort);

int main(int argc, char *argv[])
{

    int conn_s;                    /*  连接套接字      */
    short int port;                /*  端口号          */
    struct sockaddr_in servaddr;   /*  套接字地址结构   */
    char buffer[MAX_LINE];         /*  字符缓存        */
    char *szAddress;               /*  保存远程IP地址  */
    char *szPort;                  /*  保存远程端口    */
    char *endptr;

    /*  获取命令行参数  */
    ParseCmdLine(argc, argv, &szAddress, &szPort);

    /*  设置远程端口  */
    port = strtol(szPort, &endptr, 0);
    if (*endptr) {
        printf("ECHOCLNT: Invalid port supplied.\n");
        exit(EXIT_FAILURE);
    }

    /*  创建监听套接字  */
    if ((conn_s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "ECHOCLNT: Error creating listening socket.\n");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);

    /*  设置远程IP地址  */
    if (inet_aton(szAddress, &servaddr.sin_addr) <= 0) {
        printf("ECHOCLNT: Invalid remote IP address.\n");
        exit(EXIT_FAILURE);
    }

    /*  connect()连接到远程回传服务器  */
    if (connect(conn_s, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        printf("ECHOCLNT: Error calling connect()\n");
        exit(EXIT_FAILURE);
    }

    /*  从用户获取发送到回传服务器的字符串  */
    printf("Enter the string to echo: ");
    fgets(buffer, MAX_LINE, stdin);

    /*  将字符串发送给回传服务器, 并and retrieve response  */
    Writeline(conn_s, buffer, strlen(buffer));
    Readline(conn_s, buffer, MAX_LINE - 1);

    /*  输出回传的字符串  */
    printf("Echo response: %s\n", buffer);
    return EXIT_SUCCESS;
}

int ParseCmdLine(int argc, char *argv[], char **szAddress, char **szPort)
{
    int n = 1;

    while (n < argc) {
        if (!strncmp(argv[n], "-a", 2) || !strncmp(argv[n], "-A", 2)) {
            *szAddress = argv[++n];
        } else if (!strncmp(argv[n], "-p", 2) || !strncmp(argv[n], "-P", 2)) {
            *szPort = argv[++n];
        } else if (!strncmp(argv[n], "-h", 2) || !strncmp(argv[n], "-H", 2)) {
            printf("Usage:\n\n");
            printf("    timeclnt -a (remote IP) -p (remote port)\n\n");
            exit(EXIT_SUCCESS);
        }
        ++n;
    }
    return 0;
}

接下来运行程序,首先编译并在后台运行服务端程序,接着编译并带命令行参数运行客户端程序:
wu@ubuntu:~/linuxc/echosevcli$ gcc helper.c echoserv.c -o echoserv
wu@ubuntu:~/linuxc/echosevcli$ gcc helper.c echoclnt.c -o echoclnt
wu@ubuntu:~/linuxc/echosevcli$ ./echoserv 3357 &
[1] 9872
wu@ubuntu:~/linuxc/echosevcli$ ./echoclnt -a 127.0.0.1 -p 3357
Enter the string to echo: echo me, thanks.
Echo response: echo me, thanks.

wu@ubuntu:~/linuxc/echosevcli$

如果文章对您有帮助,欢迎点击下方按钮打赏作者

Comments

No comments yet.
To verify that you are human, please fill in "七"(required)