1.预备知识

1.1理解源IP地址和目的IP地址

上篇博客由唐僧的例子我们知道:
在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。

思考一下: 不考虑中间的一系列步骤,两台主机我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上。

但是我们把数据从主机A发生到主机B主机,是目的吗?
并不是目的,是手段。

真正通信的不是这两个机器!其实这是两台机器上面的软件(人)!

数据有IP标识一台唯一的主机,因此可以把一台主机上的数据交给另外一台主机。但是对方机器上不止一个程序,因此还需要有一个其他的标识来区分出,这个数据要给哪个程序进行解析。

在这里插入图片描述

那用谁来标识各自主机上客户或者服务进程的唯一性呢?

为了更好的表示一台主机上服务进程的唯一性,我们采用端口号port,标识服务器进程、客户端的唯一性!

因此用进程的唯一性和主机IP的唯一性就可以保证两台主机的服务来进行直接通信。

1.2认识端口号

端口号(port)是传输层协议的内容

虽然端口号是传输层协议的内容,但是在应用层可以被调用,通过系统调用接口,向一个进程关联上一个端口号。

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

ip地址(主机全网唯一性)+该主机上的端口号,标识该服务器上进程的唯一性

ipA+portA ----->该主机上对应的服务进程,是全网中是唯一的一个进程!
ipB+portB ----->该主机上对应的服务进程,是全网中是唯一的一个进程!

网络通信的本质其实就是进程间的通信

在这里插入图片描述

进程之间通信的本质是什么呢?

  1. 需要让不同的进程,先看到同一份资源 -----> 网络
  2. 通信不就是在做IO吗?----> 所以,我们所有上网的行为,无外乎就两种:a.我要把我的数据发出去 b. 我要收到别人给我发的数据

现在还有一些问题:

1.客户端和服务端上IP地址是不同的,但是它们两个进程关联的端口号可以一样吗?如客服端一个进程端口号是8080,服务端一个进程端口号是8080。

是可以的。

IP保证了全网唯一,port保证在主机内部的唯一性。

如果客户端和服务端在一台主机上,它们的端口号一定不能一样!

2.进程已经有pid了,为什么要有port呢?

进程已经有pid可以保证它的唯一性看起来是可以的。但是实际上不行。

a. 系统是系统,网络是网络,单独设置 ---- 系统与网络解耦

b. 需要客户端每次都能找到服务器进程 ---- 服务器的唯一性不能做任何改变 (IP+port),尤其是端口不能随意改变 —> 不能使用轻易会改变的值

c. 不是所有的进程都要提供网络服务或者请求,但是所有的进程都需要pid

3.未来进程可以和端口号(port)关联起来,我们就可以找到这台主机上网络服务进程
进程+port —> 网络服务进程
那底层OS如何根据port找到指定的进程?

实际上每个进程在OS就是PCB数据结构,端口号类型是 uint16_t,说白了就是如何通过
uint16_t --> task_struct
实际上OS采用的是hashtable方案,OS内部维护了一张基于port作Key的一张哈希表,Value就是对应的PCB的地址。只要找到了对应port就找到了对应的进程PCB然后就可以数据交付给进程。
未来我们要学习网络接口,网络接口就是文件。 拿上来的数据找到这个进程,找到这个进程就能找到它的文件描述符表,根据文件描述符表就能找到文件对象,文件对象找到了它这个文件缓冲区就找到了,然后就可以把数据拷贝到它的缓冲区里,最后就相当于网络数据放到了文件中,最后就如同读文件一样把数据读上去了。

一个端口号只能被一个进程绑定,那一个进程可以绑定多个端口号吗?
可以的。如10086的多个客服。

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;

最后一个问题:

我们在网络通信的过程中,IP+port标识唯一性,client->server,除了数据,要把自己的ip和port发给对方吗?

是的需要,我们还要发回来。未来发送数据的时候,一定会“多发”一部分数据 —> 多发的数据以协议的形式呈现。

这里可能会有些问题,第一次怎么知道对方port的?
最开始的时候服务端的端口号是不变的,并且我们用的APP等根本不是我们写的,写服务和客户端的是一家公司,客户端在写的时候它要请求的时候它客户端内置的端口号已经内置好了。而且内置好之后是绝对不变的。

1.3认识TCP协议

这里我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识, 后面在学到TCP协议在细说

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

1.4认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;,后面再详细讨论.

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

并不是说可靠就是好的,不可靠就是不好的。可靠和不可靠其实是一个中性词。
可靠是有成本的 – 往往比较复杂 --> 维护&&编码
不可靠 – 比较简单 – 维护&&使用
都有自己合适的应用场景。

1.5网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分, 那么如何定义网络数据流的地址呢?

大端是数据低地址存在内存高地址,数据高地址存内存低地址
小端是数据低地址存在内存低地址,数据高地址存内存高地址
如0x12345678 高<—低
大端机器内存放的是 12 34 56 78 低—>高
小端机器内存放的是 78 56 34 12 低—>高

如果是一个大端机把数据通过网络转给小端机,小端机把这个收到大端机的数据按照小端机存,就可能在这个服务器把数据解释反了。现在问题是作为接收方你怎么知道你接收的数据是大端还是小端?
在这里插入图片描述
也就是说不管是大端机还是小端机规定发到网络中的数据都必须是大端!

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

在这里插入图片描述

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2.socket编程接口

上面我们所说 ip+port ----->该主机上对应的服务进程,是全网中是唯一的一个进程!
ip+port就是套接字,socket

socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
 socklen_t addrlen);

sockaddr结构

我们可以看到上面struct sockaddr *addr出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:

1.网络套接字
2.原始套接字
3.unix域间套接字

网络套接字:运用于网络跨主机之间通信+本地通信
unix域间套接字: 本地通信
我们现在在使用网络编程通信时是应用层调传输层的接口,而原始套接字:跳过传输层访问其他层协议中的有效数据。主要用于抓包,侦测网络情况。。

我们现在知道套接字种类很多,它们应用的场景也是不一样的。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。

struct sockaddr_in :适用于网络通信
struct sockaddr_un:适用于域间通信

前两个字节都是16位地址类型:代表采用的是网络通信还是本地通信

未来接口里面虽然是struct sockaddr *addr,但是你要填充的是要不是struct sockaddr_in,要不是struct sockaddr_un。然后把其他一个强制类型转换传给struct sockaddr *addr。然后在内部根据struct sockaddr *addr的前两个字节判断传过来的是struct sockaddr_in还是struct sockaddr_un,然后在做强制类型转换转成对应的结构。

这里就有点像C++,父类和子类多态的意思。

在这里插入图片描述

sockaddr 结构

在这里插入图片描述

sockaddr_in 结构

在这里插入图片描述
这个结构里主要有三部分信息从上到下: 地址类型(协议家族), 端口号, IP地址.

in_addr结构

在这里插入图片描述
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数

3.UDP网络程序

3.1UDP Server服务器端

#pragma once

#include<iostream>
#include<string>

using namespace std;

//这个只能写在类外,类中static const只能定义整数
static const string defaultIP="0.0.0.0";

class udpServer
{ 
public:
    udpServer(const uint16_t& port,const string& ip=defaultIP)
    	:_port(port),_ip(ip),_sockfd(-1)
    {}
	//初始化服务器
    void initServer()
    {}
	//启动服务器
    void start()
    {}    
    ~udpServer()
    {}

private:
	//写服务器要给它ip和port
    string _ip;//服务器IP,先写成这样后面再说
    uint16_t _port;//服务器端口号port
    int _sockfd;// socket返回值时介绍
};

进行网络通信首先要创建套接字

套接字创建一套网络通信的文件机制(在文件系统层面,把对应的网卡文件打开),其实是在底层帮我打开一个文件,把文件和网卡设备关联起来。

在这里插入图片描述

domain (域):代表未来这个套接字是用来网络通信还是本地通信

在这里插入图片描述

type:套接字提供的服务类型

在这里插入图片描述

protocol:未来想使用什么协议如TCP、UDP。一般默认为0,因为前两个参数就已经帮第三个参数确定采用什么协议了。

在这里插入图片描述

成功时返回一个文件描述符,失败时返回-1,错误码被设置。Linux下一切皆文件,返回文件描述符我们知道未来所有接口大概率都跟这个值有关,通过这个文件描述符向文件中读,向文件中写。

enum //错误码类型
{
    SOCKET_ERR=2,
};

void initServer()
{
     //1.创建socket
     _sockfd=socket(AF_INET,SOCK_DGRAM,0);
     if(_sockfd == -1)
     {
         cerr<<"Server fail: "<<strerror(errno)<<endl;
         exit(SOCKET_ERR);
     }
     cout << "socket success: " << " : " << _sockfd << endl;
     
     //套接字文件创建好了只有一个文件描述符,前面说ip+port-->socket,因此需要bind绑定
     //将我们字节设置的ip和port设置到操作系统中。告诉操作系统ip和port给那个文件用的。
     //2.bind,将ip和port和套接字文件进行绑定
}

在这里插入图片描述

sockfd:调用socket返回的文件描述符
struct sockaddr:
今天我们写的是网络通信,因此需要一个struct sockaddr_in结构。里面最重要的字段有三个,
第一个是16位地址类型也叫做协议家族 AF_INET
第二个是这个服务器要绑定的16位端口号是谁
第三个是这个服务器要绑定的32位ip地址是谁

addrlen:未来要传的结构体的长度

在这里插入图片描述
成功返回0,失败返回-1

void initServer()
 {
     //1.创建socket
     _sockfd=socket(AF_INET,SOCK_DGRAM,0);
     if(_sockfd == -1)
     {
         cerr<<"Server fail: "<<strerror(errno)<<endl;
         exit(SOCKET_ERR);
     }
     cout << "socket success: " << " : " << _sockfd << endl;
     
     //2.bind
     struct sockaddr_in local;
 }

先看看这个结构

在这里插入图片描述

关于ip地址:
192.168.80.30 -----> 点分十进制的风格的IP,字符串,可读性好
但是实际上我们知道一个IP地址,这里分成4个字节,每个字节取值0-255,就可以用一个uint32_t 类型4个字节就能标识 -----> 整数类型的ip,网络通信使用

那现在就需要上面转换成下面,下面转换成上面。那该怎么转呢?
这个并不用我们转,库已经帮我们写好了,用库的就行。

不过原理我们可以看一下

在这里插入图片描述

字符串转uint_32 原理类型,把字符串分成4部分,每部分字符串一次设进p1、p2、p3、p4里,然后再把整个结构体强制类型转换赋值给uint32_t

结构体剩下的就是填充,是为了照顾结构体内存对齐的。

void initServer()
 {
     //1.创建socket
     _sockfd=socket(AF_INET,SOCK_DGRAM,0);
     if(_sockfd == -1)
     {
         cerr<<"Server fail: "<<strerror(errno)<<endl;
         exit(SOCKET_ERR);
     }
     cout << "socket success: " << " : " << _sockfd << endl;
     
     //2.bind,绑定ip+port
     struct sockaddr_in local;//定义了一个变量,在栈上,用户层
     bzero(&local,sizeof(local));//结构体内容初始设置为0
     local.sin_family=AF_INET;//采用网络通信, AF_INET填充一个struct sockaddr_in结构体,为了用于网络通信
     local.sin_port=htons(_port);//转成网络字节序, 给别人发消息,也要把自己的port和ip发送给对方

 }

在这里插入图片描述

void initServer()
 {
     //1.创建socket
     _sockfd=socket(AF_INET,SOCK_DGRAM,0);
     if(_sockfd == -1)
     {
         cerr<<"Server fail: "<<strerror(errno)<<endl;
         exit(SOCKET_ERR);
     }
     cout << "socket success: " << " : " << _sockfd << endl;
     
     //2.bind,绑定ip+port
     struct sockaddr_in local;//定义了一个变量,在栈上,用户层
     bzero(&local,sizeof(local));//结构体内容初始设置为0
     local.sin_family=AF_INET;//采用网络通信, AF_INET填充一个struct sockaddr_in结构体,为了用于网络通信
     local.sin_port=htons(_port);//转成网络字节序, 给别人发消息,也要把自己的port和ip发送给对方
     local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
    
 }

inet_addr
1.将点分十进制ip地址 字符串转整数
2.将这个整数转成网络字节序

在这里插入图片描述

现在终于把结构体填好了,但是现在OS并不知道你设置的ip和port,因为这是在用户栈上定义的,因此需要调用bind进行绑定

enum
{
    SOCKET_ERR=2,
    BIND_ERR
};

void initServer()
{
    //1.创建socket
    _sockfd=socket(AF_INET,SOCK_DGRAM,0);//这里 AF_INET,是创建一个网络通信套接字
    if(_sockfd == -1)
    {
        cerr<<"Server fail: "<<strerror(errno)<<endl;
        exit(SOCKET_ERR);
    }
    cout << "socket success: " << " : " << _sockfd << endl;

    //2.bind,绑定ip+port
    struct sockaddr_in local;//定义了一个变量,在栈上,用户层
    bzero(&local,sizeof(local));//结构体内容初始设置为0
    local.sin_family=AF_INET;//采用网络通信, AF_INET填充一个struct sockaddr_in结构体,为了用于网络通信
    local.sin_port=htons(_port);//转成网络字节序, 给别人发消息,也要把自己的port和ip发送给对方
    local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
   
    int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
    if(n == -1)
    {
        cerr<<"bind fail:"<<strerror(errno)<<endl;
        exit(BIND_ERR);
    }
}

自此UDP服务器预备工作完成。 1.创建socket 2.bind绑定

服务器初始化,要绑定ip和port,因此需要我们自己传,所以我们使用命令行参数

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_ip local_port\n\n";
}

// ./udpServer ip port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[2]);
    string ip=argv[1];
  
    unique_ptr<udpServer> ups(new udpServer(ip,port));
    ups->initServer();
    ups->start();

    return 0;
}

start先写一个空的死循环,现在这个服务器也可以启动了。

下面说一说这个自己填写的ip地址的问题

在这里插入图片描述

127.0.0.1 本地环回
未来可以使用这个地址做服务器代码测试

目前我们写的服务器在应用层,如果绑定的ip地址是127.0.0.1 ,当我做测试的时候未来发信息和读消息其实都是在本主机内,数据贯穿协议栈之后再进行流动不会到底物理层

在这里插入图片描述
服务器跑起来如何看到呢?

netstat 查看当前网络情况
netstat -nuap   n:能显示数字显示数字  u:UDP  a:all  p:进程

在这里插入图片描述

但是我们想这个服务器未来在全网服务,因此我们需要用到公网ip,但是云服务器都是虚拟化的服务器,不能直接bind你的公网ip。虚拟机或者真实的Linux环境,你可以bind你的公网ip。

在这里插入图片描述
那不能绑定公网ip,如何让别人能找到我呢?

实际情况下一款网络服务器不建议指明一个ip
就是说未来服务器不要显示绑定一个ip,因为有时候一些原因服务器上不止一个ip,如果今天绑定了一个特定ip,大家可以用的是ip1,ip2等都在向这个端口号为8080的服务器发送消息,但此时只绑定一个明确的ip,那么最终只能收到目的ip就是自己显示绑定的ip发送的数据。别人用其他ip向8080发送数据那就不能收到了。

所以在给struct sockaddr_in填充ip地址时,一般写法如下。这也是为什么在构造的时候给ip缺省值0.0.0.0的原因。 任意地址绑定!未来发送到这台机器上的所有的数据只要访问的端口号port是8080,都可以交付给这个服务器。

void initServer()
{
   //1.创建socket
   _sockfd=socket(AF_INET,SOCK_DGRAM,0);//这里 AF_INET,是创建一个网络通信套接字
   if(_sockfd == -1)
   {
       cerr<<"Server fail: "<<strerror(errno)<<endl;
       exit(SOCKET_ERR);
   }
   cout << "socket success: " << " : " << _sockfd << endl;

   //2.bind,绑定ip+port
   struct sockaddr_in local;//定义了一个变量,在栈上,用户层
   bzero(&local,sizeof(local));//结构体内容初始设置为0
   local.sin_family=AF_INET;//采用网络通信, AF_INET填充一个struct sockaddr_in结构体,为了用于网络通信
   local.sin_port=htons(_port);//转成网络字节序, 给别人发消息,也要把自己的port和ip发送给对方
   local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
   
   //local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址bind,服务器的真实写法
   
   int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
   if(n == -1)
   {
       cerr<<"bind fail:"<<strerror(errno)<<endl;
       exit(BIND_ERR);
   }
}

既可以写成注释掉的那种写法,也可以用自己写缺省值是0.0.0.0的ip,然后在构造的时候就不需要给ip传值了。这两种写法任意选择。

// ./udpServer port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<udpServer> ups(new udpServer(port));

    ups->initServer();
    ups->start();

    return 0;
}

这也是上面查看网络信息的那个图片,端口号是8080,ip地址是0.0.0.0的原因。

现在把启动服务器start写一下

服务器读取数据

在这里插入图片描述
sockfd:从那个套接字读
buf:读上来的数据放那个缓冲区
len:这个缓冲区多大
flags:怎么读,阻塞式的读取(0)
src_addr:输出型参数,今天读过来数据想知道是谁发的。返回对应的消息内容,是从哪一个client发来的。
addrlen:输出型参数,传过去的结构体多大。

因为是网络通信,因此传struct sockaddr_in结构体对象过去,会把client的ip和port消息填入这个结构体中。

在这里插入图片描述

成功时返回读取到字节的个数,失败返回-1

void start()
{
    //服务器的本质其实就是一个死循环  ---> 常驻内存的进程
    char buffer[gunm];
    for(;;)
    {
        //读数据
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);//必填
        ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        //1. 数据是什么  2.谁发的
        if(s > 0)
        {
            buffer[s]=0;
            string clientip=inet_ntoa(peer.sin_addr);//1.网络字节序 2.int->点分十进制
            uint16_t clientport=ntohs(peer.sin_port);//网络字节序转主机
            string str=buffer;

            cout<<clientip<<"["<<clientport<<"]# "<<str<<endl;
        }
    }
}

1.字节序转变 2.int->点分十进制
在这里插入图片描述

udp服务器完整代码

#pragma once

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

using namespace std;

static const string defaultIP="0.0.0.0";
const int gunm=1024;

enum
{
    USAGE_ERR,
    SOCKET_ERR=2,
    BIND_ERR
};


class udpServer
{ 
    typedef function<void(int,string,uint16_t,string)> func_t;

public:
    udpServer(const func_t& func,const uint16_t& port,const string& ip=defaultIP)
        :_callback(func),_port(port),_ip(ip),_sockfd(-1)
    {}

    void initServer()
    {
        //1.创建socket
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);//这里 AF_INET,是创建一个网络通信套接字
        if(_sockfd == -1)
        {
            cerr<<"Server fail: "<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
        cout << "socket success: " << " : " << _sockfd << endl;

        //2.bind,绑定ip+port
        struct sockaddr_in local;//定义了一个变量,在栈上,用户层
        bzero(&local,sizeof(local));//结构体内容初始设置为0
        local.sin_family=AF_INET;//采用网络通信, AF_INET填充一个struct sockaddr_in结构体,为了用于网络通信
        local.sin_port=htons(_port);//转成网络字节序, 给别人发消息,也要把自己的port和ip发送给对方
        local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
        //local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址bind,服务器的真实写法
        
        
        int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n == -1)
        {
            cerr<<"bind fail:"<<strerror(errno)<<endl;
            exit(BIND_ERR);
        }
    }

    void start()
    {
        //服务器的本质其实就是一个死循环  ---> 常驻内存的进程
        char buffer[gunm];
        for(;;)
        {
            //读数据
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);//必填
            ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            //1. 数据是什么  2.谁发的
            if(s > 0)
            {
                buffer[s]=0;
                string clientip=inet_ntoa(peer.sin_addr);//1.网络字节序 2.int->点分十进制
                uint16_t clientport=ntohs(peer.sin_port);
                string str=buffer;

                cout<<clientip<<"["<<clientport<<"]# "<<str<<endl;
            }
        }
    }

    ~udpServer()
    {}


private:
    string _ip;
    uint16_t _port;
    int _sockfd;
};
#include "udpServer.hpp"
#include <memory>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

// ./udpServer port
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<udpServer> ups(new udpServer(port));

    ups->initServer();
    ups->start();

    return 0;
}

3.2UDP Client客户端

客户端和服务端用到的接口几乎差不多,因此这里直接就包含这些头文件。

#pragma once

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

using namespace std;


class udpClient
{
public:
    udpClient(const string& ip,const uint16_t& port)
        :_serverip(ip),_serverport(port),_sockfd(-1)
    {}

    void initClient()
    {
    }

    void run()
    {
    }

    ~udpClient()
    {}

private:
    string _serverip;
    uint16_t _serverport;
    int _sockfd;
};
#include"udpClient.hpp"
#include<memory>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}

//./udpClient ip port
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
    }
    string ip=argv[1];
    uint16_t port=atoi(argv[2]);

    unique_ptr<udpClient> ups(new udpClient(ip,port));

    ups->initClient();
    ups->run();

    return 0;
}

客户端这里必须要知道服务端ip和port,然后才知道要往那个服务器发。发送到对应服务器后,只不过服务器内部在使用的时候不在看ip了,只看端口,把数据发给绑定这个端口的进程就好了。

下面初始化客服端

void initClient()
{
	//1.创建socket
    _sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(_sockfd == -1)
    {
        cerr<<"Client fail: "<<strerror(errno)<<endl;
        exit(1);
    }
    cout << "socket success: " << " : " << _sockfd << endl;
}

刚才服务端 1.创建socket 2.bind,现在问题就来了

client要不要bind?client要不要显示的bind(需不需要程序员自己bind)?

所谓bind是让套接字文件信息和网络ip和port产生关联。未来通信客户端和服务端都有自己的ip和port。那client要不要bind?

client必须要bind,但不需要显示bind(不需要程序员自己bind)

那server服务端为什么一定要显示bind?
在服务端这里bind绑定的时候,最重要的根本就不是绑定ip,最重要的而是绑定port。未来服务器要明确的port,不能随意改变。所有必须显示bind某个端口。只要服务器启动成功,一定是bind成功,它对应的端口号一定是属于它自己的,另外这个端口号是众所周知的不会轻易改变。所以需要用户显示bind。

客户端只要有port就可以,它的port是多少不重要!具有唯一性才重要!未来当客户端发信息把自己端口号填上,然后服务端能收到,然后给返回来就可以了。所以客户端不需要显示绑定。

那client客户端为什么不用显示bind?

写服务器的是一家公司,写client是无数家公司。
比如写抖音App是字节跳动一家公司,但你的手机一定装满各自APP,手机装了这么多客户端,如果每个客户端都自己说就要绑定9090这个端口号,那一定是谁先启动那个App先拿到这个端口号,那其他的客户端就启动不起来了。

所以客户端不需要明确哪一个,只需要有就可以了,保证唯一性就行了。并且这由OS自动形成端口进行bind,然后还会绑定ip

OS在什么时候,如何bind

如何bind,OS发现bind没绑就采用随机策略形成一个端口号,然后使用bind方法进行绑定。
在首次向服务器sendto数据时,OS发现没有bind绑定ip和port,只写了服务器的ip和port,所以OS会自动绑定ip和port。

客户端 1.创建socket

下面启动客服端

发送消息使用sendto接口

在这里插入图片描述

sockfd:往那个套接字发送
buf:发送的内容是什么
len:内容多长
flags:发送方式 ,阻塞式发送(0)有数据就发没数据就等
dest_addr:输入型参数,告诉客户端要发给谁。

给个struct sockaddr_in结构体,往结构体填充要发给服务器的ip地址和port。

addrlen:输入型参数,这个结构体多大

void run()
{
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_addr.s_addr=inet_addr(_serverip.c_str());
    server.sin_port=htons(_serverport);

    string str;
    while(1)
    {
        //发
        cout<<"Please Enter# ";
        string str;
        getline(cin,str);
        sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server)); 
    }
}

客户端完整代码

#pragma once

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

using namespace std;


class udpClient
{
public:
    udpClient(const string& ip,const uint16_t& port)
        :_serverip(ip),_serverport(port),_sockfd(-1)
    {}

    void initClient()
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd == -1)
        {
            cerr<<"Client fail: "<<strerror(errno)<<endl;
            exit(1);
        }
        cout << "socket success: " << " : " << _sockfd << endl;
    }


    void run()
    {
        struct sockaddr_in server;
        memset(&server,0,sizeof(server));
        server.sin_family=AF_INET;
        server.sin_addr.s_addr=inet_addr(_serverip.c_str());
        server.sin_port=htons(_serverport);

        string str;
        while(1)
        {
            cout<<"Please Enter# ";
	        string str;
	        getline(cin,str);
            sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server)); 
        }
    }

    ~udpClient()
    {}

private:
    string _serverip;
    uint16_t _serverport;
    int _sockfd;
};

4.根据UDP客户端服务端做的设计

服务器把数据读上来就完了吗?并不是,它可能还会对这些数据进行处理。
因此我们添加一个回调函数,对数据进行处理。

#pragma once

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

using namespace std;

static const string defaultIP="0.0.0.0";
const int gunm=1024;

enum
{
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR
};


class udpServer
{ 
	//包装器
    typedef function<void(int,string,uint16_t,string)> func_t;

public:
    udpServer(const func_t& func,const uint16_t& port,const string& ip=defaultIP)
        :_callback(func),_port(port),_ip(ip),_sockfd(-1)
    {}

    void initServer()
    {
        //1.创建socket
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);//这里 AF_INET,是创建一个网络通信套接字
        if(_sockfd == -1)
        {
            cerr<<"Server fail: "<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
        cout << "socket success: " << " : " << _sockfd << endl;

        //2.bind,绑定ip+port
        struct sockaddr_in local;//定义了一个变量,在栈上,用户层
        bzero(&local,sizeof(local));//结构体内容初始设置为0
        local.sin_family=AF_INET;//采用网络通信, AF_INET填充一个struct sockaddr_in结构体,为了用于网络通信
        local.sin_port=htons(_port);//转成网络字节序, 给别人发消息,也要把自己的port和ip发送给对方
        local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
        //local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址bind,服务器的真实写法
        
        
        int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n == -1)
        {
            cerr<<"bind fail:"<<strerror(errno)<<endl;
            exit(BIND_ERR);
        }
    }

    void start()
    {
        //服务器的本质其实就是一个死循环  ---> 常驻内存的进程
        char buffer[gunm];
        for(;;)
        {
            //读数据
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);//必填
            ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            //1. 数据是什么  2.谁发的
            if(s > 0)
            {
                buffer[s]=0;
                string clientip=inet_ntoa(peer.sin_addr);//1.网络字节序 2.int->点分十进制
                uint16_t clientport=ntohs(peer.sin_port);
                string str=buffer;

                cout<<clientip<<"["<<clientport<<"]# "<<str<<endl;

                //并不是收到数据打印就完了,还要就行业务处理
                _callback(_sockfd,clientip,clientport,str);
            }
        }
    }

    ~udpServer()
    {}


private:
    string _ip;
    uint16_t _port;
    int _sockfd;

    func_t _callback;//回调函数
};

就可以在handerMessage里对message进行特定的业务处理,而不关心message怎么来的。从而实现server通信和业务逻辑解耦!

#include "udpServer.hpp"
#include <memory>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
}

// ./udpServer port
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<udpServer> ups(new udpServer(handerMessage,port));
    ups->initServer();
    ups->start();

    return 0;
}

注意云服务器的网络端口默认都是基本关闭的!需要你自己打开。不然别人客户端根据这个ip和port也找不到这个服务器。

4.1字典+热加载

字典就是做中文和英文直接的翻译,因此需要一个一对一映射关系,所以我们选择unordered_map容器做为字典。

首先要给字典初始化,在容器中插入一些中文与英文的映射关系。因此我们可以自己创建一个dict.txt文件,然后从文件中读,这里可以采用C++关于对文件的操作,ifstream用起来很方便。while这里的判断在C++的IO流说过,这里不再细说。

const string filename = "dict.txt";
unordered_map<string, string> dict;

void initDict()
{
	//读
    ifstream iss(filename);
    string str, Key, Val;
    while (iss >> str)
    {
        // 分割字符串
        //传入的是输出型参数
        if (CurString(str, &Key, &Val, ":"))
        {
            dict.insert(make_pair(Key, Val));
        }
    }
    cout << "load dict success" << endl;
}

把每次读过来的字符串做分割,这里我们以 " : " 作为分隔符,把分割好的Key,Val插入到容器中。

bool CurString(string &str, string *s1, string *s2, string step)
{
    auto pos = str.find(step.c_str());
    if (pos != string::npos)
    {
        *s1 = str.substr(0, pos);  //[)前闭后开
        *s2 = str.substr(pos + 1); //[)前闭后开
        return true;
    }
    else
    {
        return false;
    }
}

初始化词典完成之后,就可以进行业务处理了

// demo1  翻译
void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    string renopose_str;
    auto it = dict.find(message);
    if (it != dict.end())
    {
        renopose_str += it->second;
    }
    else
    {
        renopose_str = "UnKnow";
    }

    // 返回
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);

    sendto(sockfd, renopose_str.c_str(), renopose_str.size(), 0, (struct sockaddr *)&client, sizeof(client));
}

完整代码

#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

const string filename = "dict.txt";
unordered_map<string, string> dict;

bool CurString(string &str, string *s1, string *s2, string step)
{
    auto pos = str.find(step.c_str());
    if (pos != string::npos)
    {
        *s1 = str.substr(0, pos);  //[)
        *s2 = str.substr(pos + 1); //[)
        return true;
    }
    else
    {
        return false;
    }
}

void initDict()
{
    ifstream iss(filename);
    string str, Key, Val;
    while (iss >> str)
    {
        // 分割字符串
        if (CurString(str, &Key, &Val, ":"))
        {
            dict.insert(make_pair(Key, Val));
        }
    }

    cout << "load dict success" << endl;
}


// demo1  翻译
void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    string renopose_str;
    auto it = dict.find(message);
    if (it != dict.end())
    {
        renopose_str += it->second;
    }
    else
    {
        renopose_str = "UnKnow";
    }

    // 返回
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);

    sendto(sockfd, renopose_str.c_str(), renopose_str.size(), 0, (struct sockaddr *)&client, sizeof(client));
}

现在服务器把翻译返回给客户端了,那客户端也得能接收读取。

客户端这里我们只需要改变一个地方就可以了

void run()
{
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_addr.s_addr=inet_addr(_serverip.c_str());
    server.sin_port=htons(_serverport);

    string str;
    while(1)
    {
        //demo1
        //发
        cout<<"Please Enter# ";
        cin>>str;
        sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));

		
		//收
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"Server provide translate: "<<buffer<<endl;
        }
    }
}

这样简单的一个字典就写好了,还有一个热加载是什么意思呢?
对于目前这个字典来说,热加载的意思是不需要重启服务器,就能使在文件中新增的内容立即生效。

具体可以这样做,可以对某个信号进行捕捉设置一个捕捉方法,然后再里面调用字典初始化函数。这样就完成了热加载。

void reload(int signo)
{
    (void)signo;
    initDict();
}

// ./udpServer port
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    // demo1 翻译
    //热加载
    signal(3,reload);//对3号信号进行捕捉
    initDict();
    
    unique_ptr<udpServer> ups(new udpServer(handerMessage,port));

    ups->initServer();
    ups->start();

    return 0;
}

4.2shell命令行

现在做的是,想在客户端输入一些指令

ls -a -l
pwd
touch text.txt
...

希望服务端能在自己的服务器上执行成功并且把结构返回给客户端。
我们这里写的是简单的,有些命令不能执行。

现在自己想改这份代码怎么改呢?不要这份翻译了。
是不是只需要再写一个完成这样任务的业务逻辑就好了,这就是解耦的好处!

这里我们需要用popen接口,这样就不需要我们像以前实现myshell那样,创建子进程然后在子进程中调用exec*系列的函数执行程序替换,那样麻烦了。

在这里插入图片描述

popen相当于做了pipe+fork+exec*的工作
command:未来要执行的命令字符串
返回类型FILE * :通过管道以文件的方式把对应的执行结果写到文件中
type:对这个文件以什么方式打开 “w”、“r”等等

在这里插入图片描述

失败返回NULL,要不是fork创建子进程失败,要不pipe创建管道失败,要不内存申请失败

// demo2  命令
void execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
	//1.cmd解析  ls -a -l
	//2.如果必要可能需要 fork,exec*

	//对于一些执行不执行
    if (cmd.find("rm") != string::npos || cmd.find("mv") != string::npos || cmd.find("rmdir") != string::npos)
    {
        cerr << clientip << ":" << clientport << " 正在做一个非法的操作: " << cmd << endl;
        return;
    }

    string response;
    FILE *pf = popen(cmd.c_str(), "r");//以读的方式打开文件
    if (pf == NULL)
        response = cmd + "excel fail";
    char line[1024];
    while (fgets(line, sizeof(line) - 1, pf))//每次从文件中读一行
    {
        response += line;
    }
    
	pclose(pf);
	
	//发
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);

    sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&client, sizeof(client));	
}
// ./udpServer port
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    // demo2 命令
    unique_ptr<udpServer> ups(new udpServer(execCommand, port));

    ups->initServer();
    ups->start();

    return 0;
}

客户端也还是改那个地方就好了

void run()
{
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_addr.s_addr=inet_addr(_serverip.c_str());
    server.sin_port=htons(_serverport);

    string str;
    while(1)
    {
        //demo2
        //发
        cout<<"[wdl@VM-28-3-centos 24test_3_16]$ ";
        string str;
        getline(cin,str);
        sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));

		//收
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        /ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(n>0)
        {
            buffer[n]=0;
            cout<<buffer<<endl;
        }           
    }
}

4.3聊天室

下面我们写一个能群聊的客户端服务器。客户端发来一个消息想让服务器做一个转发,让所有在线的人都能收到这个消息,然后自己也能接收到别人的消息。

正常来说我们的服务器应该写一个用户注册登录功能,但是这里不想搞那么麻烦。
这里这样做,如果客户端发 “online” 就加入群聊,然后发的消息就由服务器推送给群在线的所有人也可以就收别人发的消息,客户端发 “offline” 退出群聊。

因此我们首先写一个用户管理的模块 onlineUser.hpp

#pragma once

#include<iostream>
#include<string>
#include<unordered_map>

using namespace std;

//用户管理
class User
{
public:
    User(const string& ip,const uint16_t& port)
        :_ip(ip),_port(port)
    {}

    ~User()
    {}

    string& Getip()
    {
        return _ip;
    }

    uint16_t& Getport()
    {
        return _port;
    }

private:
    string _ip;
    uint16_t _port;
};


//在线用户管理
class onlineUser
{
public:
    onlineUser()
    {}

    void addUser(const string& ip,const uint16_t& port)
    {
        string id=ip+"#"+to_string(port);
        usp.insert(make_pair(id,User(ip,port)));
    }

    void eraseUser(const string& ip,const uint16_t& port)
    {
        string id=ip+"#"+to_string(port);
        usp.erase(id);
    }

    bool isOnline(const string& ip,const uint16_t& port)
    {
        string id=ip+"#"+to_string(port);
        return usp.find(id) != usp.end() ? true:false;
    }

	//给每一个在线用户转发某个客户端发的消息
    void boradcast(int sockfd,const string& ip,const uint16_t& port,const string& msg)
    {
        for(auto& us:usp)
        {
            struct sockaddr_in client;
            memset(&client,0,sizeof(client));
            client.sin_family=AF_INET;
            client.sin_addr.s_addr=inet_addr(us.second.Getip().c_str());
            client.sin_port=htons(us.second.Getport());
            string s=ip+"-"+to_string(port)+"# ";//用来标识是那台主机发的
            s+=msg;//发的信息
			//转发
            sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&client,sizeof(client));
        }
    }

    ~onlineUser()
    {}

private:
    unordered_map<string,User> usp;

};
static onlineUser onlineuser;

//demo3 聊天室
void routeMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    if(message == "online") onlineuser.addUser(clientip,clientport);

    if(message == "offline") onlineuser.eraseUser(clientip,clientport);


    if(onlineuser.isOnline(clientip,clientport))
    {
        onlineuser.boradcast(sockfd,clientip,clientport,message);
    }
    else
    {
        struct sockaddr_in client;
        bzero(&client, sizeof(client));
        client.sin_family = AF_INET;
        client.sin_addr.s_addr = inet_addr(clientip.c_str());
        client.sin_port = htons(clientport);
        string s="你还没有上线,请先上线,运行: online";

        sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr *)&client, sizeof(client));
    }
}
// ./udpServer port
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    //demo3 聊天室
    unique_ptr<udpServer> ups(new udpServer(routeMessage, port));

    ups->initServer();
    ups->start();

    return 0;
}

客户端这里我们做一些设计,可能你发一条消息之后不在发了,但是还在群聊里,别人发的信息我也应该能收到,也不能把发和读放在一块,因为它们都是阻塞式等待。所以这里我们写一个线程。一个线程读,一个线程写。

class udpClient
{
public:
	//...
	
	//读线程
	static void* readMessage(void* args)
	{
	    int sockfd=*(static_cast<int*>(args));
	
	    while(true)
	    {
	        char buffer[1024];
	        struct sockaddr_in peer;
	        socklen_t len=sizeof(peer);
	        ssize_t n=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
	        if(n>=0)
	        {
	            buffer[n]=0;
	            cout<<buffer<<endl;
	        }
	    }
	
	    return nullptr;
	}
	
	
	void run()
	{
	
	    //demo3 读
	    pthread_create(&_pid,nullptr,readMessage,(void*)&_sockfd);
	    pthread_detach(_pid);
	    
	
	    struct sockaddr_in server;
	    memset(&server,0,sizeof(server));
	    server.sin_family=AF_INET;
	    server.sin_addr.s_addr=inet_addr(_serverip.c_str());
	    server.sin_port=htons(_serverport);
	
	    string str;
	    while(1)
	    {
	        //demo3 
	        //发
	        cerr<<"Enter# ";//
	        string str;
	        getline(cin,str);
	        sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server)); 
	    }
	}
	
private:
    string _serverip;
    uint16_t _serverport;
    int _sockfd;

    pthread_t _pid;//线程ID

};

客户端这里还有一个问题,因为现在这里我们窗口就一个,你发送的消息和接收的消息就会揉在一起看起来比较乱。因此我们创建一个管道文件。把客户端收到的消息打印的时候都重定向到这个管道文件中,然后我们在开一个端口从管道文件中读,这样就把发送和接收也分开了。就不会揉在一起了。

所以在发的时候我们这样写,cerr<<"Enter# "; 我们知道编译器默认会打开三个文件,标准输入,标准输出,标准错误,我们这里是把标准输出重定向到管道文件,但标准错误并没有改变,因此使用cerr,可以在用户发信息的时候看到这个提示。

在这里插入图片描述

5.windows客户端与linux服务端交汇

windows环境下实现客户端和我们在linux上写服务端使用的socket套接字接口一模一样,但是有三处不一样的地方

windows环境下要进行套接字方面的编程要需要使用库的,在安装vs的时候就已经有了。因此首先要包含头文件

#include<WinSock2.h>

#pragma comment(lib,"ws2_32.lib")//windows socket 2_版本 32位 lib库

其次要对WinSocket初始化

WSAStartup启动WinSocket,MAKEWORD构建一个2.2库的版本,把构建出来的结果放到wsd中。这里就有点像你的客户端有版本,自己写的版本在启动的时候windows要和导入的库的版本进行对比。

int main()
{
	 WSAData wsd;//初始化信息
	
	 //启动Winsock
	 if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {/*进行WinSocket的初始化,
	     windows 初始化socket网络库,申请2, 2的版本,windows socket编程必须先初始化。*/
	     cout << "WSAStartup Error = " << WSAGetLastError() << endl;
	     return 0;
	 }
	 else {
	     cout << "WSAStartup Success" << endl;
	 }
	//...
}

最后关闭socket并清理Winsock

int main()
{
	//...
	
	closesocket(sockfd);
    WSACleanup();
    
	return 0;
}

剩下的在linux怎么写就在windows怎么写

客户端

#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:4996) //vs会报一个错误,这里是屏蔽掉这个错误
#include<iostream>
#include<string>
#include<cstring>
#include<thread>
#include<WinSock2.h>

#pragma comment(lib,"ws2_32.lib")

using namespace std;

//服务器ip,端口
const string serverip = "124.223.54.148";
uint16_t serverport = 8080;

int main()
{
    WSAData wsd;//初始化信息

    //启动Winsock
    if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {/*进行WinSocket的初始化,
        windows 初始化socket网络库,申请2, 2的版本,windows socket编程必须先初始化。*/
        cout << "WSAStartup Error = " << WSAGetLastError() << endl;
        return 0;
    }
    else {
        cout << "WSAStartup Success" << endl;
    }

    //1.创建套接字
    SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd  == SOCKET_ERROR) {
        cout << "socket Error = " << WSAGetLastError() << endl;
        return 1;
    }
    else {
        cout << "socket Success" << endl;
    }

    //创建收消息的线程
    thread t1([&]()
        {
            char buffer[1024];
            while (true)
            {
                struct sockaddr_in peer;
                int len = sizeof(peer);
                int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
                if (n > 0)
                {
                    buffer[n] = 0;
                    cout << "server reponose: " << buffer << endl;
                }
                else break;

            }
        }
    );

    //发
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    string msg;

    while (true)
    {
        cout << "Please Enter# ";
        getline(cin, msg);
        int n=sendto(sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&server, sizeof(peer));
        if (n < 0)
        {
            cerr << "sendto error!" << endl;
            break;
        }
    }

    //清理
    closesocket(sockfd);
    WSACleanup();

	return 0;
}

服务端

udpServer.hpp一点没变

udpServer.cc变了一点点

#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>
#include <stdio.h>

void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}


void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    string renopose_str;

    // 返回
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);

    sendto(sockfd, renopose_str.c_str(), renopose_str.size(), 0, (struct sockaddr *)&client, sizeof(client));
}


// ./udpServer port
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
    }
    uint16_t port = atoi(argv[1]);
    // string ip=argv[1];

    unique_ptr<udpServer> ups(new udpServer(handerMessage, port));


    ups->initServer();
    ups->start();

    return 0;
}

多平台涉及到编码方式不一样,但是我们这里是简单实现的,没有考虑这个问题,因此不要发中文。

在这里插入图片描述

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐