Cpp实现CS架构

本文最后更新于:5 个月前

阿里嘎多,来为计网实验做个准备

本文代码运行环境为 linux系统

你可以在我的github仓库中找到源代码,或者在本文底部 可以star一波 小声bb

要构建一个应用程序

  • 首先要选择它的结构,有两种典型架构: CS Client-ServerP2P peer to peer。其中CS架构是我们最常接触和使用到的。

  • 接下来我们要实现进程间通信。 这就是今天我们要干的事情:在CS架构下实现 Client与server的通信。

网络进程间通信是通过 socket(应用层与运输层之间的接口) 来向网络 发送/接收 报文实现的。 大多数语言都为我们提供了socket API。 我们能做的就是 使用这些API,选择运输层参数,实现通信。

UDP实现CS

在这部分我们实现一个 server将client发送内容转化为大写并返回的功能

UDP_server

首先分析一下 UDP协议下server需要做什么。

  1. 创建socket,绑定自己的ip和端口。 这是server和client都需要做的 应用层必须按协议栈向下运行才能实现通信。而socket正是这个应用层与下层的接口。
  2. 从socket读取client发送来的内容。
  3. 通过socket,确定目的地(client的ip和端口号),发送响应内容。

接下来我们来一步步实现

创建、初始化socket

//创建Udp_socket,socket函数来自 sys/socket.h, IPPOROTO_UDP来自netinet/in.h
int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

socket函数原型为:

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

af代表地址族(IP),一般为ipv4或ipv6

其中AF_INET代表IPV4 ip地址,SOCK_DGRAM表示是无保障传输,protocol即运输协议。

我们在这里就是声明了一个UDP的socket

for more information

为了能让client能访问到我们,我们使用bind函数进行绑定,将server的套接字与一个IP和一个端口相连。

struct sockaddr_in server_addr;
   //     struct sockaddr_in{
   //     sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
   //     uint16_t        sin_port;     //16位的端口号
   //     struct in_addr  sin_addr;     //32位IP地址
   //     char            sin_zero[8];  //不使用,一般用0填充
   // };
   //		struct in_addr{
   //   		 in_addr_t  s_addr;  //32位的IP地址
   //		};
   memset(&server_addr,0,sizeof(server_addr));
   server_addr.sin_family=AF_INET;
   server_addr.sin_port=htons(1234);//转为大端序
   server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//function from arpa/inet.h

   bind(Udp_socket,(struct sockaddr*)&server_addr,sizeof(server_addr));

这里一下子多了很多内容,我们来看一下。

首先是结构体 sockaddr_in,他的内容已在上面代码写出。

​ 首先是 sin_family成员,和sock函数的第一个参数含义相同,取值也要相同。IPV4, AF_INET

​ sin_port为要绑定的端口号。理论取值为 0 ~65536,但因为 0 ~1023端口一般都分配给特点的应用,所 以尽量避免使用。 可以在这个范围里随意选一个

​ sin_addr竟然也是一个结构体…用来绑定IP

bind函数原型:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux

关于sockaddr和sockaddr_in的区别详见: 这里

准备接受数据

芜湖,我们终于结束了折磨人的初始化环节,接下来就简单了!

我们首先要申请缓冲区来读写数据,并申请一些变量。就像Linux中通过文件描述符读写文件也需要缓冲区一样

char recv_buf[50];//接受缓冲区
char send_buf[50];//发送缓冲区
int recv_num;//接收字节数
int send_num;//发送字节数
struct sockaddr_in client_addr;//客户地址
int len=sizeof(client_addr);//客户地址长度

为了将数据发送给client,我们也需要他的IP和端口号这就是在这里声明一个 client_addr的原因。

开始接/发数据

while(true){
    cout<<"waiting for data"<<endl;
    //从client端接收数据
    recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&client_addr,(socklen_t *)&len);
    //检验错误
    if(recv_num<0)
    {
        cout<<"receive ERROR"<<endl;
        exit(1);
    }
    recv_buf[recv_num]='\0';//添加字符串结束符
    cout<<"Receive "<<recv_num<<"bytes: "<<recv_buf<<endl;
    //实现server功能,即转化为大写。
    for(int i=0;i<recv_num;i++){
        if(recv_buf[i]>='a'&&recv_buf[i]<='z')
        send_buf[i]=toupper(recv_buf[i]);
    }
    send_buf[recv_num]='\0';
    //发送转化完的数据
    send_num=sendto(Udp_socket,send_buf,recv_num,0,(struct sockaddr*)&client_addr,len);
    if(send_num<0)
    {
        cout<<"send ERROR"<<endl;
        exit(1);
    }
}

可以看到使用了recvfrom和sendto两个函数,分别用来接、发数据

recvfrom函数原型

int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from,int *fromlen);

s为socketfd,即server当前使用的套接字

buf为接收缓冲区,即声明的数组

len为缓冲区大小,一般使用sizeof表示

flags参数一般不用,设置为0

from为client端地址,使用 *(struct sockaddr)&client_addr**的形式

fromlen为指针,指向from缓冲区长度值。

sendto函数原型

int sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);

与recvfrom类似,只是将buf(接收缓冲区)换成了msg(发送缓冲区);将fromlen指针直接换成了长度参数。

for more information

最后只需要关闭socket。(非常像fd!

close(Udp_socket);

UDP_client

与server类似,我们也先来分析一下client需要干什么事情。

  • 创建socket(有socket才能通信),绑定server的ip和端口。
  • 准备发送数据。
  • 通过socket发送数据,并接收反馈数据。

创建、初始化socket

这部分client和server有很大的不同。

server中,我们首先将socket与自己的IP和端口绑定了;但在client中,我们并没有声明任何有关自己的地址变量。

目前可以从 UDP的特点进行解释:

UDP是不面向连接的传输协议,每一个socket由一个二元组(目的IP和目的Port)决定,而且在server中,通过recvfrom函数是可以获得client的IP和Port的。

所以此时我们只要确定目的IP和Port就可以唯一在server中确定一个socket来接受数据。

注意:这样的情况只有在CS架构下才可能实现:

如果在P2P架构下,每个人都会是server,如果不绑定自己的IP和Port,别就无法向你发送数据。

而CS架构下由于client一直为发出数据后再接收;server一直为 接收数据后再发送。所以server并不需要自己知道自己的IP和Port,只需要让server知道即可。

int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

   struct sockaddr_in server_addr;
   memset(&server_addr,0,sizeof(server_addr));
   server_addr.sin_family=AF_INET;
   server_addr.sin_port=htons(1234);
   server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");

准备接受数据

创建缓冲区……balabala….

int len=sizeof(server_addr);
 char send_buf[50];
 char recv_buf[50];
 int send_num,recv_num;
 cout<<"please input string!"<<endl;
 cin>>send_buf;

开始发/接数据

和上文中server类似,只不过是收发顺序颠倒一下,少了一个功能实现部分。

    send_num=sendto(Udp_socket,send_buf,strlen(send_buf),0,(struct sockaddr*)&server_addr,len);
    if(send_num<0){
        cout<<"send ERROR"<<endl;
        exit(1);
    }
    recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf),0,(struct sockaddr*)&server_addr,(socklen_t*)&len);
    if(recv_num<0){
        cout<<"receive ERROR"<<endl;
        exit(1);
    }
    recv_buf[recv_num]='\0';
    cout<<"Server give back: "<<recv_buf<<endl;
    
    close(Udp_socket);
    return 0;
}

TCP实现CS

TCP协议与UDP协议的一大不同就是: TCP时面向连接传输层协议,而UDP是不面向连接的。

二者的类比就像是: TCP是打电话,UDP是写信。即一个会建立专线,另一个和大家共享资源。

UDP协议只是在IP上面很简单地封装了一层,而TCP要更加复杂。

不管怎么,在这篇博客中我们并不需要关心底层,我们只需要知道API就行了 :happy:

另外,请记住,TCP是面向连接的!

TCP_Client

相信看过UDP程序后你对socket编程已经很有心得了,本部分我们就简单地实现一个server在收到消息后简单地print一句话给client的naive程序。

你会注意到这次我先编写的Client程序,原因是TCP协议下用户和服务器连接需要用户先于服务器 “握手” 创建连接,这样的顺序应该会更符合逻辑

创建、初始化socket

注意将 socket()函数中的type和protocol参数更改为稳定数据传输和TCP

其他和UDP_client的内容基本相同,也是不需要绑定自己的IP和Port

//创建套接字
        int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        //向服务器(特定的IP和端口)发起请求
        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
        serv_addr.sin_family = AF_INET;  //使用IPv4地址
        serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
        serv_addr.sin_port = htons(1234);  //端口
        connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

创建连接&准备发/收数据

由于我们实现的功能没有发数据,所以就少了一些内容。

注意,connect函数就是client用来和server建立连接的过程

connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
      
       //读取服务器传回的数据
       char buffer[40];
       read(sock, buffer, sizeof(buffer)-1);
      
       printf("Message form server: %s\n", buffer);
      
       //关闭套接字
       close(sock);
       return 0;

connect函数原型:

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

sockfd即指定数据发送的套接字;

server_addr即指定数据发送地址;

addrlen即指定server_addr结构体的长度;

然后我们就可以通过read函数将取服务器发回的数据读入缓冲区中

TCP_Server

创建、初始化socket

注意将 socket()函数中的type和protocol参数更改为稳定数据传输和TCP

其他和UDP_server的内容基本相同。

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        //将套接字和IP、端口绑定
        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
        serv_addr.sin_family = AF_INET;  //使用IPv4地址
        serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
        serv_addr.sin_port = htons(1234);  //端口
        bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

接受数据

由于我们实现的功能只有print,所以就不需要预先设定缓冲区

//进入监听状态,等待用户发起请求
        listen(serv_sock, 20);
        //接收客户端请求
        struct sockaddr_in clnt_addr;
        socklen_t clnt_addr_size = sizeof(clnt_addr);
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
        //向客户端发送数据
        char str[] = "hello client!";
        write(clnt_sock, str, sizeof(str));

这里出现了listen函数,即监听函数,这是对client中connect函数的一个照应。

listen函数原型:

int listen ( int sockfd,  int backlog );

sockfd即被 listen函数作用的套接字。是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数。

backlog 即server同时建立连接的上限值。

然后同accept函数接受client的请求

accept函数作用:接收一个套接字中已建立的连接。

原型:

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

emmm,相信大家都能看懂,不然就来这。

发送数据

//向客户端发送数据
       char str[] = "hello client!";
       write(clnt_sock, str, sizeof(str));
      
       //关闭套接字
       close(clnt_sock);
       close(serv_sock);
       return 0;
   }

通过write向client写数据。

最后关闭socket即可。

Source code

UDP_server

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
using namespace std;

int main()
{
    //创建Udp_socket,socket函数来自 socket.h, IPPOROTO_UDP来自in.h
    int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

    struct sockaddr_in sever_addr;
    //     struct sockaddr_in{
    //     sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    //     uint16_t        sin_port;     //16位的端口号
    //     struct in_addr  sin_addr;     //32位IP地址
    //     char            sin_zero[8];  //不使用,一般用0填充
    // };
    memset(&sever_addr,0,sizeof(sever_addr));
    sever_addr.sin_family=AF_INET;
    sever_addr.sin_port=htons(1234);
    sever_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//function from arpa/inet.h
    bind(Udp_socket,(struct sockaddr*)&sever_addr,sizeof(sever_addr));

    cout<<"Sever is ready to response!!!"<<endl;

    char recv_buf[50];//接受缓冲区
    char send_buf[50];//发送缓冲区
    int recv_num;
    int send_num;
    struct sockaddr_in client_addr;
    int len=sizeof(client_addr);
    while(true){
        cout<<"waiting for data"<<endl;
        recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&client_addr,(socklen_t *)&len);
        if(recv_num<0)
        {
            cout<<"receive ERROR"<<endl;
            exit(1);
        }
        recv_buf[recv_num]='\0';
        cout<<"Receive "<<recv_num<<"bytes: "<<recv_buf<<endl;
        for(int i=0;i<recv_num;i++){
            if(recv_buf[i]>='a'&&recv_buf[i]<='z')
            send_buf[i]=toupper(recv_buf[i]);
        }
        send_buf[recv_num]='\0';
        send_num=sendto(Udp_socket,send_buf,recv_num,0,(struct sockaddr*)&client_addr,len);
        if(send_num<0)
        {
            cout<<"send ERROR"<<endl;
            exit(1);
        }
    }
    close(Udp_socket);
    return 0;
}

UDP_Client

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

using namespace std;

int main()
{
    int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

    struct sockaddr_in sever_addr;
    memset(&sever_addr,0,sizeof(sever_addr));
    sever_addr.sin_family=AF_INET;
    sever_addr.sin_port=htons(1234);
    sever_addr.sin_addr.s_addr=inet_addr("127.0.0.1");

    int len=sizeof(sever_addr);
    char send_buf[50];
    char recv_buf[50];
    int send_num,recv_num;
    cout<<"please input string!"<<endl;
    cin>>send_buf;

    send_num=sendto(Udp_socket,send_buf,strlen(send_buf),0,(struct sockaddr*)&sever_addr,len);
    if(send_num<0){
        cout<<"send ERROR"<<endl;
        exit(1);
    }
    recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf),0,(struct sockaddr*)&sever_addr,(socklen_t*)&len);
    if(recv_num<0){
        cout<<"receive ERROR"<<endl;
        exit(1);
    }
    recv_buf[recv_num]='\0';
    cout<<"Sever give back: "<<recv_buf<<endl;
    
    close(Udp_socket);
    return 0;
}

TCP_Client

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
   
    //读取服务器传回的数据
    char buffer[40];
    read(sock, buffer, sizeof(buffer)-1);
   
    printf("Message form server: %s\n", buffer);
   
    //关闭套接字
    close(sock);
    return 0;
}

TCP_server

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);
    //接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    //向客户端发送数据
    char str[] = "hello client!";
    write(clnt_sock, str, sizeof(str));
   
    //关闭套接字
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!