所谓client/server简单来说就是客户端服务器模型,简称C/S模型,一个简单的CS模型所用到的只是一些简单的网络方面的知识,下面我以一个面向连接的CS实例来解释面向连接的主要过程:

首先我们想看一张图,来了解一下服务器端和客户端的链接过程:

client的定义 docker client


首先是服务器端,服务器首先要创建套接字,然后将其绑定到本地端口,之后将其转换为链接套接字,之后就时阻塞等待客户端的连接了。

在这里解释一下端口和IP地址之间的关系:我们都知道IP地址就如同家庭的地址和门牌号,能够找到某人的家,这里就如同找到某台主机是一样的,但是家里的人不止一个,就如同一台主机同时运行的应用程序有好多一样,要将消息准发到特定的应用程序就需要用到端口号,端口号就是该应用程序消息的接口。一般情况下,某个应用程序的端口是特定的,例如:浏览器的端口是80一样。
那么套接字又是干什么的呢?套接字中保存有客户端课服务器端的IP地址和连接套接字,有了连接套接字就能将信息发出了。

客户端同时也需要建立一个连接套接字,在客户端将IP地址和端口等信息初始化完成之后,就需要将客户端与服务器连接,此时服务器处于阻塞状态等待客户端的连接,当客户端连接成功之后,就会产生一个连接套接字,消息通过连接套接字发送和接收。

上图读取数据和发送数据就是通过send函数和recv函数来实现的。

在服务器和客户端之间一般是发送一些特定的数据包,例如整个结构体数据,我们可以利用memcpy函数将结构体保存在一个字符串数组当中,然后等到在客户端接收之后,再将其还原为一个结构体。

下面是实例的代码:
客户端

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include "my_recv.h"

#define INVALID_USERINFO    'n'     //用户信息无效
#define VALID_USERINFO      'y'     //用户信息有效

int get_userinfo(char *buf, int len)                //获取用户输入存入buf,长度len,以'\n'结束
{
    int i;
    int c;

    if(buf == NULL)
    {
        return -1;
    }

    i = 0;
    while(((c = getchar()) != '\n') && (c != EOF) && (i < len - 2))
    {
        buf[i++] = c;
    }
    buf[i++] = '\n';
    buf[i++] = '\0';

    return 0;
}


void input_userinfo(int conn_fd, const char *string)                //输入用户名,然后通过fd发送出去
{
    char input_buf[32];
    char recv_buf[BUFSIZE];
    int  flag_userinfo;

    do
    {                                                             //输入用户信息知道正确为止 
        printf("%s:", string);
        if(get_userinfo(input_buf, 32) < 0)
        {
            printf("error return from get_userinfo\n");
            exit(1);
        }

        if(send(conn_fd, input_buf,strlen(input_buf), 0) < 0)
        {
            my_err("send", __LINE__);
        }

        if(my_recv(conn_fd, recv_buf, sizeof(recv_buf)) < 0)
        {
            printf("data is too long\n");
            exit(1);
        }

        if(recv_buf[0] == VALID_USERINFO)
        {
            flag_userinfo = VALID_USERINFO; 
        }
        else
        {
            printf("%s error, input again,", string);
            flag_userinfo = INVALID_USERINFO;
        }
    }while(flag_userinfo == INVALID_USERINFO);
}

int main(int argc, char ** argv)
{
    int                  i;
    int                  ret;
    int                  conn_fd;
    int                  serv_port;
    struct sockaddr_in   serv_addr;
    char                 recv_buf[BUFSIZE];

    if(argc != 5)                                   //检查参数个数
    {
        printf("usage : [-p] [serv_port] [-a] [serv_address]\n");
        exit(1);
    }

    memset(&serv_addr, 0, sizeof(struct sockaddr_in));        //初始化服务器端地址结构
    serv_addr.sin_family = AF_INET;
    for(i = 1; i < argc; i++)                                   //从命令行获取服务器的端口与地址
    {
        if(strcmp("-p", argv[i]) == 0)
        {
            serv_port = atoi(argv[i+1]);
            if(serv_port < 0 || serv_port > 65535)
            {
                printf("invalid serv_addr.sin_port\n");
                exit(1);
            }
            else 
            {
                serv_addr.sin_port = htons(serv_port);
            }
            continue;
        }
        if(strcmp("-a", argv[i]) == 0)
        {
            if(inet_aton(argv[i+1], &serv_addr.sin_addr) == 0)
            {
                printf("invalid server ip address\n");
                exit(1);
            }
            continue;
        }
    }

    if(serv_addr.sin_port == 0 || serv_addr.sin_addr.s_addr == 0)           //检测是否少输入了某项参数
    {
        printf("usage: [-p] [serv_addr.sin_port] [-a] [serv_address]\n");
        exit(1);
    }

    conn_fd = socket(AF_INET, SOCK_STREAM, 0);                              //创建一个tcp套接字
    if(conn_fd < 0){
        my_err("socket", __LINE__);
    }

    if(connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) < 0){    //向服务器端发送连接请求
        my_err("connect", __LINE__);
    }

    input_userinfo(conn_fd, "username");                    //输入用户名和密码
    input_userinfo(conn_fd, "password");

    if((ret = my_recv(conn_fd, recv_buf, sizeof(recv_buf))) < 0){   //读取欢迎信息并打印
        printf("data is too long\n");
        exit(1);
    }
    for(i = 0; i < ret; i++){
        printf("%c", recv_buf[i]);
    }
    printf("\n");

    close(conn_fd);
    return 0;

}

服务器端:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include "my_recv.h"
#include<stdlib.h>

#define SERV_PORT       4507        //服务器的端口
#define LISTENQ         12          //连接请求队列的最大长度

#define INVALID_USERINFO    'n'     //用户信息无效
#define VALID_USERINFO      'y'     //用户信息有效

#define USERNAME            0       //接受到的是用户名
#define PASSWORD            1       //接受到的是密码

struct userinfo                     //保存用户名和密码的结构体
{
    char username[32];
    char password[32];
};

struct userinfo users[] = 
{
    {"linux", "unix"},
    {"4507", "4508"},
    {"clh", "clh"},
    {"xl", "xl"},
    {" ", " "}                      //以只含一个空格的字符串作为数组的结束标志
};

int find_name(const char * name)
{
    int  i;

    if(name == NULL)
    {
        printf("in find_name, NULL pointer");
        return -2;
    }
    for(i = 0; users[i].username[0] != ' '; i++)
    {
        if(strcmp(users[i].username, name) == 0)
        {
            return i;
        }
    }
    return -1;
}

void send_data(int conn_fd, const char *string)
{
    if(send(conn_fd, string, strlen(string), 0) < 0)
    {
        my_err("send", __LINE__);   //my_err函数在my_recv.h中声明            
    }
}

int main()
{
    int                  sock_fd, conn_fd;
    int                  optval;
    int                  flag_recv = USERNAME;
    int                  ret;
    int                  name_num;
    pid_t                pid;
    socklen_t            cli_len;
    struct sockaddr_in   cli_addr, serv_addr;
    char                 recv_buf[128];

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);          //创建一个TCP套接字
    if(sock_fd < 0)
    {
        my_err("socket", __LINE__);
    }

    optval = 1;                                         //设置该套接字使之可以重新绑定端口
    if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(int)) < 0)
    {
        my_err("setsocketopt", __LINE__);
    }

    memset(&serv_addr, 0, sizeof(struct sockaddr_in));   //初始化服务器端地址结构
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(sock_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0)                        //将套接字绑定到本地端口
    {
        my_err("bind", __LINE__);
    }

    if(listen(sock_fd, LISTENQ) < 0)                    //将套接字转化为监听套接字
    {
        my_err("listen", __LINE__);
    }

    cli_len = sizeof(struct sockaddr_in);
    while(1)
    {
        conn_fd = accept(sock_fd, (struct sockaddr *)&cli_addr, &cli_len);
        if(conn_fd < 0)
        {
            my_err("accept", __LINE__);
        }

        printf("accept a new client, ip: %s\n", inet_ntoa(cli_addr.sin_addr));
        if((pid = fork()) == 0)                         //创建子进程处理刚刚接收的连接请求
        {
            while(1)                                    //子进程
            {
                if((ret = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)
                {
                    perror("recv");
                    exit(1);
                }
                recv_buf[ret - 1] = '\0';               //将数据结束标志'\n'替换成字符串结束标志

                if(flag_recv == USERNAME)               //接收到的是用户名
                {
                    name_num = find_name(recv_buf);
                    switch (name_num)
                    {
                        case -1:
                            send_data(conn_fd, "n\n");
                            break;
                        case -2:
                            exit(1);
                            break;
                        default:
                            send_data(conn_fd, "y\n");
                            flag_recv = PASSWORD;
                            break;
                    }
                }
                else if(flag_recv == PASSWORD)          //接收到的是密码 
                {
                    if (strcmp(users[name_num].password, recv_buf) == 0)
                    {
                        send_data(conn_fd, "y\n");
                        send_data(conn_fd, "welcome login my tcp server\n");
                        printf("%s login \n", users[name_num].username);
                        break;
                    }
                    else 
                    {
                        send_data(conn_fd, "n\n");
                    }
                }
            }
            close(sock_fd);
            close(sock_fd);
            exit(0);                                    //结束子进程            
        }
        else
        {
            close(conn_fd);                             //父进程关闭刚刚接收的连接请求,执行accept等待其他连接请求
        }
    }
    return 0;
}

my_recv.h

#ifndef _MY_RECV_H
#define _MY_RECV_H
    #define BUFSIZE 1024
    void my_err(const char * err_string, int line);
    int my_recv(int conn_fd, char * data_buf, int len);
#endif

这里自定义了一个读取数据的函数,实际就是将套接字缓冲区的数据拷贝到自定义缓冲区(以”\n”为结束标志),然后再按格式将数据读出(效果和上面所说的memcpy函数是一样的)。

在该例中,当收到一个新的客户端的连接请求之后,服务器端就会创建一个新的进程来处理客户端的请求。关于进程创建可以参考进程控制。

在该客户端,首先创建了一个TCP套接字,然后调用函数connect请求与服务器端连接,建立连接之后,通过连接套接字首先发送用户名,然后等待服务器确认,若用户存在,则发送密码,若密码正确,则返回欢迎页面。

下面是可执行文件的链接地址:github

执行本程序时,首先在某一终端运行服务器端程序,然后在另外几个终端运行客户端。在客户端执行时输入如下数据:
./client -a 127.0.0.1 -p 4507

服务器中默认存在用户,用户名和密码分别是:
{“linux”, “unix”}, {“4507”, “4508”}, {“clh”, “clh”}, {“xl”, “xl”}。