网络基础与进阶

认识网络协议

认识协议

        实际上协议的本质是约定。类似于计算机生产厂商有很多; 计算机操作系统, 也有很多; 计算机网络硬件设备, 还是有很多; 如何让这些不同厂商之间生产的计算机能够相互顺畅的通信? 就需要有人站出来, 约定一个共同的标准, 大家都来遵守, 这就是 网络协议;

协议的分层

理解分层

分层最大的好处在于 "封装"

每一层都要解决特定的问题

OSI七层模型
OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型, 是一个逻辑上的定义和规范;
把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机; OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;
它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯; 

但它既复杂又不实用;我们实际现实中用的都是下面的TCP/IP五层协议

TCP/IP五层模型

1.物理层 
        负责光/ 电信号的传递方式。   比如现在以太网通用的网线 ( 双绞线 ) 、早期以太网采用的同轴电缆 (现在主要用于有线电视 ) 、光纤 , 现在的 wifi 无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。 集线器 (Hub) 工作在物理层。
2.数据链路层 
        负责设备之间的数据帧的传送和识别。   例如网卡设备的驱动、帧同步 ( 就是说从网线上检测 到什么信号算作新帧的开始) 、冲突检测 ( 如果检测到冲突就自动重发 ) 、数据差错校验等工作。   有以太网、令牌环网, 无线 LAN 等标准。   交换机 (Switch) 工作在数据链路层
3.网络层

         负责地址管理和路由选择。例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由).。路由器(Router)工作在网路层。

4.传输层

        负责两台主机之间的数据传输。如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机。

5.应用层

         负责应用程序间沟通。如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。 我们的网络编程主要就是针对应用层。

OSI&&TCP/IP参考图示 

网络的传输基本流程

补充概念

此处先补充一些概念,便于后面理解网络传输的流程

协议报头
  • 协议每一层都有,而每一个协议的最终表现就是协议都要有报头
  • 协议通常是通过协议报头来进行表达的
  • 每一份数据最终在被发送或者在不同的协议层中,都要有自己的报头
  • 报头是可以帮助传输协议进行正确路由和处理数据。(类似快递单,里面有该数据要传到哪和从哪传的等等属性)
局域网  
  1. 两台局域网的主机能够直接通信
  2. 局域网通信的原理

此处只是初识,为了理解之后流程

每一台机器都有自己的“名字”,每一台主机都有网卡,每一张网卡都有自己的地址,MAC地址(物理地址)表明自己在局域网中的唯一性。

以下是数据传输的参考图 

        假设MAC1向MAC6发送数据(你好),这一份数据中不仅仅有你好这一句话,还包含了MAC1和MAC6地址,并且发送数据到网络中,每一台主机都收到这份数据,但内部判断是MAC6的,所以除了MAC6,其他主机都不做处理。

        在局域网中,只允许一台主机在任何一个时刻中发送信息。否则会发送碰撞,因此,局域网也称为碰撞域。这里有一个令牌环网,类似于锁,谁拿到令牌环网,谁才能发送消息。

传输流程

以下是传输流程参考图,信息量有点多,下面我来按顺序解释一下。

        报文 = 报头  +  有效载荷

        用户A和用户B用着各自主机,用户A为客户端,用户B为服务端,客户端向服务端发送信息。每一层都要加上该层的报头(下面①②③④分别表示的以太网协议报头,ip协议报头,TCP协议报头,FTP协议报头),为了观看的更清楚,我保留的加报头的痕迹,但实际是直接传入到下一层的,不是拷贝。

        客户端发送的信息从应用层到以太网驱动程序依次封装上报头,所以该过程称为封装过程。最后传到网络中,网络中有许多主机,而它们内部处理后,发现不是发送给它们主机的将不做处理,只有B主机拿到该数据。

        然后接下来的过程是相反的从以太网到应用层,从下至上依次解包(解包),并且将有效载荷交付到上层(分用),这个过程称为解包分用过程。最后用户B拿到该信息。

        跨网段的主机的文件传输。 数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器。以下是参考图示。
        这张图注意路由器那一部分,以太网要交付搭配IP层必须要先解包,解包解的是以太网协议报头,然后交付到路由器,路由器识别报头要传到是哪一个主机,然后识别完后要交付到主机不能直接交付,要向下做封装,而这时封装的报头就不是以太网报头,而是令牌环驱动报头。                                                                                                                                         这张图和上一张图对比,可以看到除了最底层,其他层都是一样的,最底层在从以太网到路由器要做 解包操作。而路由器到令牌环要 重新封装,这两个操作做到了屏蔽底层网络差异,而ip层从发送方,中间路由器到接收方看到的全部都是一样的,所以ip层以及ip协议存在的意义就是 屏蔽底层网络的差异。

一个设备至少要横跨两个网络,才能实现数据报跨越网络转发,所以路由器必须要横跨至少两个网络,所以路由器必须要有两个网络接口。

数据包封装和分用

  • 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame)。
  • 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装
    (Encapsulation)。
  • 首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息。
  • 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议字段" 将数据交给对应的上层协议处理。

网络中的地址管理

认识IP地址

  • IP协议有两个版本,IPv4和IPv6。
  • IP地址是在IP协议中,用来标识网络中不同主机的地址;
  • 对于IPv4来说,IP地址是一个4字节,32位的整数;
  • 我们通常也使用 "点分十进制" 的字符串表示IP地址,例如 192.168.0.1;用点分割的每一个数字表示一个字节,范围是 0 - 255;

认识MAC地址

  • MAC地址用来识别数据链路层中相连的节点;
  • 长度为48位,及6个字节。一般用16进制数字加上冒号的形式来表示(例如:08:00:27:03:fb:19)。
  • 在网卡出厂时就确定了,不能修改。mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址,可能会冲突; 也有些网卡支持用户配置mac地址)。

IP && MAC

        IP地址类似于人在地球上的住址(可以更换门牌号),而MAC地址则是设备的身份证(固定不变)。它们在不同的层级和网络环境中发挥了各自的作用斌确保数据的准确传输。

源IP地址和目的IP地址

源IP是指发起通信的节点的IP地址,它标识了通信的发起方,在网络通信中,源IP地址用于识别数据包的来源,以便目的节点能够将回复发送给正确的位置。
目的IP是指接收通信的节点的P地址,它标识了通信的接收方,在网络通信中,目的IP地址用于指定数据包的目的地,以便网络设备能够将数据包传递到正确的位置。

认识端口号port

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

IPA + portA 标识该主机上对应的服务进程在全网中是唯一的一个进程。IP保证全网唯一,port保证在主机内部的唯一性。

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

1.而进程间通信的前提是需要让不同的进程,先看到同一份资源-----网络。

2.通信其实就是在做IO。所以我们的上网行为:①要把我的数据发送出去

                                                                           ②要收到别人给我发送的数据

问题1

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

  1. 系统是系统,网络是网络,单独设置,减少耦合,不互相影响。-----系统与网络解耦
  2. 启动客户端每次都要能找到服务器进程。所以服务器的唯一性不能做任何改变,因此port不能随意改变,不能使用轻易会改变的值。
  3. 不是所有的进程都要提供网络服务或者请求,但所有的进程都需要pid-----不是所有的进程都要有port,但所有的进程都要有pid。

问题2

底层操作系统是如何根据port找到指定进程的?

它的底层是一个hash,key值为port,value值为进程pid的地址。

根据key值找value。

源端口号和目的端口号

源端口就是指本地端口
目的端口就是远程端口

TCP && UDP

认识TCP

TCP(Transmission Control Protocol 传输控制协议 )
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

认识UDP

UDP(User Datagram Protocol 用户数据报协议 )
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

TCP && UDP

这里的可靠和不可靠不代表褒义词和贬义词,是一个中性词,看使用场景。

1.可靠是有成本的,往往比较复杂-----体现在维护和编码。

2.不可靠,往往比较简单-----体现在维护和使用。

网络字节序

        内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。

大端 && 小端

比如向内存地址为0x1000的地址写入0x12345678这个四字节16进制数。

对于大端模式,即低地址存放高字节数:

对于小端模式,即低地址存放低字节数:

问题

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

接口

为使网络程序具有可移植性, 使同样的 C 代码在大端和小端计算机上编译后都能正常运行, 可以调用以下库函数做网络字节序和主机字节序的转换。
#include  <arpa/inet.h>
uint32_t  htonl (uint32_t  hostlong);
uint16_t  htons (uint16_t  hostshort);
uint32_t  ntohl (uint32_t  netlong);
uint16_t  ntohs (uint16_t  netshort);
h表示host,n表示network,l表示32位长整数,s表示16位短整数
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

网络套接字socket常见接口与下文代码中用到的接口 

socket

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器 )
int socket(int domain, int type, int protocol);
参数:int  domain 通信域
我们主要看前两个AF_UNIX,AF_LOCAL为本地通信
AF_INET 为网络通信
参数:int  type
参数:int  protocol 协议
例如前两个参数已经填完了,已经确定了使用TCP或者UDP通信。则这个参数也已经固定了,可以设为0忽略。
返回值:成功返回打开的文件描述符fd
              失败返回-1

bind

// 绑定端口号 (TCP/UDP, 服务器 )
int bind(int sockfd,const struct sockaddr *address,socklen_t address_len);
参数:int  sockfd 文件描述符
一般为socket函数返回的值
参数: const struct sockaddr *address
bind和accept和connect都包含了这个参数,我们单独拿出来讲。
参数:addrlen ,表示 addr 结构体的大小。
返回值:成功返回0
              失败返回-1

accept

// 接收请求 (TCP, 服务器 )
int accept(int socket,struct sockaddr* address,socklen_t* address_len);
功能: 当服务器监听到客户端的连接请求时,它会调用 accept() 函数来接受连接。 accept()  函数接受客户端连接请求,创建一个新的套接字,该套接字用于与客户端通信。
参数: const struct sockaddr *address
bind和accept和connect都包含了这个参数,我们单独拿出来讲。
参数:addrlen,表示 addr 结构体的大小。

connect

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

功能:客户端通常通过 connect() 函数来建立与服务器的连接。connect() 函数向服务器的IP地址和端口发起连接请求。如果服务器正在监听这个端口,连接请求将传递给服务器。

参数: const struct sockaddr *address 
bind和accept和connect都包含了这个参数,我们单独拿出来讲。
参数:addrlen ,表示 addr 结构体的大小。

listen

// 开始监听socket (TCP, 服务器 )
int listen(int socket,int backlog);
功能: 当服务器套接字调用  listen()  函数后,它进入监听状态。这意味着服务器已准备好接受客户端的连接请求。
参数:
socket :文件描述符
backlog :
  • 是在进入队列中等待被处理的连接的最大数量。
  • 当有新的客户端连接请求时,如果已经有 backlog 个连接处于等待状态,新的连接请求将被拒绝。

bzero&&memset

#include <strings.h>

void bzero(void *s, size_t n);

功能:将参数s所指向的内存区域前n个字节全部设为0.

参数:

s为内存指针

n为需要清0的字节数

#include

void *memset(void *s, int v, size_t n); 与bzero用法相似

功能:将参数s所指向的内存区域前n个字节全部设为v.

参数:

这里s可以是数组名,也可以是指向某一空间的指针;

v为要填充的值;填为0则为与bzero功能一样

n为要填充的字节数;

inet_addr&&inet_ntoa

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

in_addr_t  inet_addr(const char* cp);

功能:传入一个点分十进制0.0.0.0字符串,将它转成uint32_t的整数,再将整数转成网络序列。调用这个接口可以让它帮我们实现两件事。1.string -> uint32_t          2.htonl()

参数 cp:点分十进制字符串0.0.0.0

char* inet_ntoa(struct  in_addr  in);

功能:用于接收数据后,将ip转成点分十进制,可读性好。调用这个接口可以让它帮我们实现两件事。1.uint32_t -> string          2.ntohl()

参数:in为结构体sockaddr_in结构体变量中的sin_addr.

总结:点分十进制风格的ip仅仅只是可读性好,整数的风格的ip网络通信使用,只需4个字节

recvfrom&&sendto

#include <sys/types.h>

#include <sys/socket.h>

ssize_t recvfrom(int  sockfd, void*  buf, size_t  len, int  flags, struct  sockaddr*  src_addr, socklen_t*  addrlen);

功能:通过sockfd文件描述符从对方接收数据,用于UDP协议。

参数:

  • sockfd : 文件描述符
  • buf :接收数据的首地址,数据读在哪一个缓冲区中。
  • len : 可接受数据的最大长度
  • flags :0:默认方式接收

                   MSG_OOB : 接收带外数据

                   MSG_PEEK : 查看数据标志,返回的数据并不在系统中删除,如果再次调用                                                  recv 函数会返回相同的数据内容。

                    MSG_DONTWAIT : 设置为非阻塞操作

                    MSG_WAITALL : 强迫接收到len的大小的数据后才返回,除非有错误或有信号                                                    产生。

  • src_addr : 存放发送方的IP地址和端口(输入输出型参数)返回对应的数据内容,是从哪一个客户端发来的,我们的自己创建一个struct sockaddr_in  peer。可以理解成缓冲区,传入函数里,结构体的字段都由操作系统设置。
  • addrlen :src_addr长度(输入输出型参数)

返回值:

        成功:实际发送的字节数

        失败:-1

ssize_t  sendto (int  sockfd, const  void*  buf, size_t  len, int  flags,  const  struct  sockaddr*  dest_addr, socklen_t  addrlen);

功能 :sendto 函数将数据发送到特定目标。

参数 :

  • sockfd : 文件描述符
  • 指向包含要传输的数据的缓冲区的指针。
  • buf 参数指向的数据的长度(以字节为单位)。
  • flags :0: 默认方式发送
                MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
                MSG_OOB:指明发送的是带外信息
  • dest_addr:指向包含目标套接字地址的sockaddr结构的可选指针。
  • addrlen : 由 dest_addr参数指向的地址的大小(以字节为单位)。

popen&&pclose

#include <stdio.h>

FILE*  popen (const  char*  command, const  char*  type);

功能:pipe + fork + exec* 三合一的接口,传入指令(ls -a -l), 通过fork创建子进程,再在内部exec*程序替换,最后通过管道,以文件的格式返回结果。

参数:

        command :linux指令

        type : r (读)  w(写)  a(连加)

int  pclose (FILE*  stream)

功能:关闭文件流。 

sockaddr结构

        先看后两种类型的套接字,sockaddr_in和sockaddr_un。如果要设计接口则需要每一个设计一套,但比较麻烦,所以设计者设计了一个sockaddr类型的套接字,也就是第一个。可以看到上图红色框里面,前两个字节都是一样的,前两个字节是用来区分类型的。
        我们可以看到如果要使用第一种类型来同步其他两个类型,类型转换是无法避免的,例如我们使用网络通信sockaddr_in,每一个字段填完数据后,传到参数中,发现类型不同,就应该进行强制类型转换。
        类型转换之后,传入sockaddr中,内部处理,提取前2个字节判断是sockadd_in或sockaddr_un,判断确定后,在内部再将sockaddr转成对应类型,按这种方式设计出一套通用的套接字接口。
sockadd_in结构

下面是sockadd_in的定义

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
  };

对每一个字段进行解释一下:

第一行 : __SOCKADDR_COMMON (sin_);  //地址类型

传入的是_sin。跳转到它的定义,看到宏定义。

这里补充一下##的用法,将左符号和右符号合并为新符号,例如传进来一个_sin而##后面是一个family,最终__SOCKADDR_COMMON (sin_)的值就是一个sa_family类型的sin_family.

#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

第二行 : in_port_t  sin_port对应的是uint16_t类型的端口号。下面是in_port_t的定义。

typedef uint16_t in_port_t;

第三行 : struct  in_addr  sin_addr对应的是一个结构体变量,结构体里面封装了一个uint32_t ip。

typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

UDP

前面铺垫了那么多,下面我们进入有趣的代码环节,做几个好玩的小实验,代码有点长。

以下三个代码用的都是127.0.0.1——本地环回,可以用ifconfig指令查看。

如果客户端和服务器只在一个主机上,只是做一个服务器代码的测试。只会把应用层,传输层,网络层,数据链路层四层走一个来回,捕获到达物理层,也就是还没发出去。

但是以下三份代码均可以在本机开启服务端,然后将客户端的可执行文件发给其他人,让他们参与进来,客户端启动时连上服务端的服务器公网ip即可!!

如果你的服务器没有开启端口号,应该先在你买的服务器网页开放端口。服务器配置安全组开放端口。

demo1

翻译功能,客户端输入英文,服务端传回中文。

udpServer.cc   服务端

//udpServer.cc  服务端
#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>

using namespace Server;
using namespace std;

unordered_map<string, string> dict;

const string dict_path = "./dict.txt";

const string seg = ":"; // 文件中的分隔符

static void Usage(string proc)
{
    cout << "Usage : " << proc << "  local_port" << endl;
    exit(USAGE_ERR);
}

static bool cutString(const string &line, string *key, string *value)
{
    // apple:苹果
    auto pos = line.find(seg);
    if (pos != string::npos)
    {
        // 找到
        *key = line.substr(0, pos);
        *value = line.substr(pos + seg.size()); // 加上分隔符的长度以防分隔符有空格或其他
        return true;
    }

    else
    {
        cout << "请查看词典的分隔符seg格式,修改分隔符格式" << endl;
        return false;
    }
}

static void loadDict() // 加载文件到unordered_map中
{
    // 读取数据
    ifstream in(dict_path, ios::binary); // 二进制读取
    if (!in.is_open())
    {
        // 打不开
        cerr << "open file error : " << errno << strerror(errno) << endl;
        exit(OPEN_ERR);
    }

    string line;
    string key, value;
    while (getline(in, line)) // 将按行分隔后的字符串写入到line变量中
    {
        if (cutString(line, &key, &value))
        {
            dict.insert(make_pair(key, value));
        }
    }

    cout << "load  success " << endl;

    in.close();
}

static void reload(int signo)
{
    (void)signo;
    loadDict();
}

// demo1
static void handlerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    // 对message进行特定的业务处理,而不关心message怎么来的 —— server通信和业务逻辑解耦
    string respond;
    auto it = dict.find(message);
    if (it == dict.end())
    {
        respond = "unknow";
    }

    else
    {
        respond = it->second;
    }

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

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

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }

    // string ip = argv[1];
    uint16_t port = atoi(argv[1]);

    //demo1
    signal(2, reload); // 捕获2号信号
    loadDict();


    unique_ptr<udpServer> udpSV(new udpServer(handlerMessage, port));

    udpSV->initServer();
    udpSV->start();
    return 0;
}

udpServer.hpp   服务端

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <functional>

namespace Server
{
    using namespace std;

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

    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        OPEN_ERR
    };

    static const string g_ip = "0.0.0.0";
    static const int g_num = 1024;
    class udpServer
    {
    public:
        udpServer(const func_t &cb, const uint16_t &port /*,const string &ip = g_ip*/)
            : _port(port)
            , _callback(cb)
            , _sockfd(-1) // 默认设为-1失败
        {
        }

        void initServer()
        {
            // 1.创建socket
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 前面两个参数设置好之后第三个可默认设置为0,因为已经固定了
            if (_sockfd == -1)
            {
                cerr << "socket error : " << errno << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }

            cout << "socket  success : " << _sockfd << endl;

            // 填充结构体字段
            struct sockaddr_in local;      // 定义一个in_addr的类型的结构体变量
            bzero(&local, sizeof(local));  // 将结构体中的数据清空
            local.sin_family = AF_INET;    // 协议家族 通信方式用网络通信AF_INET
            local.sin_port = htons(_port); // 端口号

            // 服务器的真实写法
            local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind; htonl  ip为0.0.0.0 只要发的是给端口号8080的都是发可以是发给我这台机器

            // 2.绑定port,ip
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n == -1)
            {
                cerr << "bind error : " << errno << strerror(errno) << endl;
                exit(BIND_ERR);
            }
        }

        void start()
        {
            // 服务器本质是死循环
            char buffer[g_num];
            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; // s为实际发送的字节数 添加\0
                    string client_ip = inet_ntoa(peer.sin_addr);
                    uint16_t client_port = ntohs(peer.sin_port);

                    string messages = buffer;
                    cout << client_ip << "[" << client_port << "]  >>>  " << messages << endl;
                    _callback(_sockfd,client_ip,client_port,messages);//读出数据后对数据做处理
                }
            }
        }

        ~udpServer()
        {
        }

    private:
        uint16_t _port; // 端口号
        string _ip;
        int _sockfd; // 文件描述符
        func_t _callback;
    };
}

udpClient.cc   客户端

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

using namespace Client;
using namespace std;

static void Usage(string proc)
{
    cerr<<"Usage : "<<proc<<"  server_ip  server_port"<<endl;
    exit(1);
}

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> udpCLI(new udpClient(port,ip));

    udpCLI->initClient();
    udpCLI->run();
    
    return 0;
}

udpClient.hpp  客户端

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <pthread.h>

namespace Client
{
    using namespace std;
    class udpClient
    {
    public:
        udpClient(const uint16_t &port, const string &ip)
            : _serverip(ip), _serverport(port), _sockfd(-1), _quit(false)
        {
        }

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

            cout << "socket  success : " << _sockfd << endl;
            // 客户端要bind,但不需要自己bind,操作系统bind
        }

        static void *readMessage(void *args) // 用多线程 实现读写分离,如果用户不想发信息,也可以收到别人发的消息
        {
            int sockfd = *(static_cast<int *>(args));
            pthread_detach(pthread_self());

            while (true)
            {
                char buffer[1024];
                struct sockaddr_in tmp;
                socklen_t tmp_len = sizeof(tmp);
                ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &tmp_len);
                if (n >= 0)
                    buffer[n] = 0;
                cout << buffer << endl;
            }

            return nullptr;
        }

        void run()
        {
            pthread_create(&reader, nullptr, readMessage, (void *)&_sockfd);

            struct sockaddr_in server;
            memset(&server, 0, sizeof(server)); // 作用跟bzero一样
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_family = AF_INET;
            server.sin_port = htons(_serverport);

            string message;
            char cmdline[1024];
            while (!_quit)
            {
                // cerr << "Please Enter>>>";//用cerr原因:与读线程的文件描述符错开 读用cout:1 写用cerr:2
                // cin>>message;
                fprintf(stderr,"#");//用stderr原因:与读线程的文件描述符错开 读用cout:1 写用cerr:2
                fflush(stderr);
                fgets(cmdline, sizeof(cmdline), stdin);
                cmdline[strlen(cmdline) - 1] = 0;
                message = cmdline;

                sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
            }
        }

        ~udpClient()
        {
        }

    private:
        int _sockfd;
        uint16_t _serverport;
        string _serverip;

        pthread_t reader;

        bool _quit;
    };
}

dict.txt文件,这里只是测试,所以词汇量只有几个,可以自己补充。

按这个格式补充,或者可以其他格式,那就需要修改seg。

apple:苹果
hello:你好
dict:字典
success:成功

效果展示:

可以看到客户端向服务端发送英文,服务端返回中文的效果。并且有一个细节问题。

细节问题
服务端与客户端bind问题

服务端是否要手动bind和是否要显示的bind。

在服务端需要手动bind,最关键的并不是绑定ip,而是绑定端口,未来服务器要明确的port,不能随意改变。

客户端要bind,但不需要自己bind。

客户端要向服务端发信息,所以服务端一点要明确端口号。而客户端只要有端口号就可以,能在发信息把端口号填上就可以,便于之后知道是哪一个客户端发的即可。写服务器是一家公司,而写客户端是无数家公司,如果你绑定一个固定的port,而另一家公司比你先绑定了这个port,则导致你绑定不上。有操作系统字段形成端口进行bind。所以客户端需要bind,但是不用自己bind,是操作系统帮我们bind。操作系统在如果你创建了套接字,而没有调用bind,操作系统会自动随机形成一个port给你绑定。

服务端ip绑定问题

举个例子:若我只绑定了127.0.0.1本地环回,另外两个ip(公网ip,内网ip)的接收到的报文都不会被上传交付到上层,都被丢弃,只有本地环回的传得上来,所以我们用服务器的真实写法,只要端口号为我们设置的8080(举例),都会被上传到上层,不管是服务器上的ip。

 local.sin_addr.s_addr = htonl(INADDR_ANY);

INADDR_ANY:任意地址bind;ip为0.0.0.0 只要发的是给端口号8080的都是发可以是发给我这台机器 .

捕获2号信号,自定义操作

设计一个信号捕获,然后加载新的词典,服务器不想退出,然后想在词典中加入单词,并能获取到。

static void loadDict() // 加载文件到unordered_map中
{
    // 读取数据
    ifstream in(dict_path, ios::binary); // 二进制读取
    if (!in.is_open())
    {
        // 打不开
        cerr << "open file error : " << errno << strerror(errno) << endl;
        exit(OPEN_ERR);
    }

    string line;
    string key, value;
    while (getline(in, line)) // 将按行分隔后的字符串写入到line变量中
    {
        if (cutString(line, &key, &value))
        {
            dict.insert(make_pair(key, value));
        }
    }

    cout << "load  success " << endl;

    in.close();
}

static void reload(int signo)
{
    (void)signo;
    loadDict();

效果展示:

demo2

实现一个客户端向服务端发送linux指令,服务端接收后并返回指令执行结果。

在上一份代码的udpServer.cc加上以下函数,并且做以下图片的修改。

static void execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
    string respond;
    FILE *fp = popen(cmd.c_str(), "r");
    if (fp == nullptr)
    {
        respond = cmd + "exec failed";
    }

    else
    {
        char line[1024];
        while (fgets(line, sizeof(line) - 1, fp))
        {
            respond += line;
        }
    }

    pclose(fp);

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

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

效果展示:

demo3

设计一个群聊系统,客户端与客户端之间互相能看到互相发送的消息,实现群聊的功能。

一样,在udpServer加上以下的函数,并且做跟demo2一样的修改,将execCommand改成routeMessage。

onlineUser onlineuser;//创建一个全局对象
static void routeMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    if (message == "online") // 登录
    {
        onlineuser.addUser(clientip, clientport);
    }

    if(message == "off")// 退出登录
    {
        onlineuser.delUser(clientip,clientport);
    }

    if (onlineuser.isOnline(clientip, clientport)) // 在线
    {
        onlineuser.broadCast(sockfd,clientip,clientport,message);
    }

    else // 还未登录
    {
        struct sockaddr_in client;
        bzero(&client, sizeof(client));
        client.sin_addr.s_addr = inet_addr(clientip.c_str());
        client.sin_family = AF_INET;
        client.sin_port = htons(clientport);

        string respond = "您还未登录,请先登录——输入online  退出off";

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

并且带上一个onlineUser.hpp,在udpServer.cc中包含上#include "onlineUser.hpp"

然后就可以开始玩了。

#pragma once

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

using namespace std;

class User
{
public:
    User(const string &ip, const uint16_t &port)
        : _ip(ip), _port(port)
    {
    }

    ~User()
    {
    }

    string IP()
    {
        return _ip;
    }

    uint16_t Port()
    {
        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) + "]";

        users.insert(make_pair(id, User(ip, port)));
    }

    void delUser(const string &ip, const uint16_t &port)
    {
        string id = ip + " [" + to_string(port) + "]";
        users.erase(id);
    }

    bool isOnline(const string &ip, const uint16_t &port)
    {
        string id = ip + " [" + to_string(port) + "]";

        return users.find(id) == users.end() ? false : true;
    }

    void broadCast(int sockfd, const string &ip, const uint16_t &port, const string &message)
    {
        for (auto &user : users)
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));
            client.sin_addr.s_addr = inet_addr(user.second.IP().c_str());
            client.sin_family = AF_INET;
            client.sin_port = htons(user.second.Port());

            string s = ip + "[" + to_string(port) +"] " + ">>";
            s += message;
            sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr *)&client, sizeof(client));
        }
    }

    ~onlineUser()
    {
    }

private:
    unordered_map<string, User> users;
};

效果展示:

此处我借助管道文件来展示效果,以下操作都是本机操作,但不代表不能跨主机跨网络通信。

这里的服务端启动后挂在后台即可,客户端1已经登录,客户端2未登录hello无法传达到客户端1中。接着往下看。

如你所见,通信起来了,客户端1端口号为34566,客户端2端口号为33372。这里的服务器存在感挺低的,启动之后就挂在后台就可以。接下来可以自己试一试,启动前先用mkfifo指令创建两个管道文件,还是那句话以下操作都是本机操作,但不代表不能跨主机跨网络通信。可以拉上你们的朋友一起玩玩。

细节问题
双线程执行

为了模拟真实感,实现我如果不想发信息,也可以看到别人发的信息,只潜水。代码中用了双线程的方法,一个线程执行写,一个线程执行读,互相不干扰。

主线程执行写。

副线程执行读,执行readMessage函数

与读线程的文件描述符错开

为了不互相干扰,与读线程的文件描述符错开使用,读线程使用1 而写线程用2

并且fgets读取键盘输入,它会读取回车,也就是\n。需要我们手动去掉,

cmdline[strlen(cmdline) - 1] = 0;将\n置为0。

demo4

多加一个实现一个linux当服务端,在windows下写一个客户端,用windows连接linux服务器。在网上找的udp在windows上实现通信的源代码,做一点改动。内容不多解释。

这个代码对比上面的代码改动比较大。下面代码是客户端的代码,注意是在windows的编译器编写的,我用的是visual studio 2019编译器。

#pragma warning(disable:4996) //屏蔽错误 可能编译器的识别问题 不加有报错

#include<iostream>
#include<string>
#include<string.h>
#include<WinSock2.h>

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

using namespace std;

string serverip = "8.130.31.160";
uint16_t serverport = 8080;

int main()
{
	WSAData wsd;
	//启动WinSock
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "WSAStartup error = " << WSAGetLastError() << endl;
		return 0;
	}

	else
	{
		cout << "WSAStartup Success " << endl;
	}

	SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock == SOCKET_ERROR)
	{
		cout << "socket error = " << WSAGetLastError()<< endl;
		return 0;
	}

	else
	{
		cout << "socket success" << endl;
	}

	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = inet_addr(serverip.c_str());
	server.sin_port = htons(serverport);

#define NUM 1024
	char inbuffer[NUM];
	string line;
	while (true)
	{
		//发送
		cout << "Please Enter  >>> ";
		getline(cin, line);
		int n = sendto(sock, line.c_str(), line.size(),0 ,(struct sockaddr*)&server, sizeof(server));
		if (n < 0)
		{
			cout << "sendto failed" << endl;
			break;
		}

		//收取
		struct sockaddr_in peer;
		int len = sizeof(peer);
		inbuffer[0] = 0;
		n = recvfrom(sock, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&peer, &len);
		if (n > 0)
		{
			inbuffer[n] = 0;
			cout << "server return message  >>> " << inbuffer << endl;
		}

		else
		{
			break;
		}
	}

	closesocket(sock);
	WSACleanup();
	return 0;
}

 下面的代码是udpServer.cc的代码,只需要保留下面的代码即可,其他全删掉。再带上udpServer.hpp(不做修改)。这个我们不用linux的客户端,所以linux上的客户端代码都不用带。接下来我们看一下效果。

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

using namespace Server;
using namespace std;

static void Usage(string proc)
{
    cout << "Usage : " << proc << "  local_port" << endl;
    exit(USAGE_ERR);
}

// demo1
static void handlerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    string res_message = message;
    res_message += "[server respond]";
    // 返回
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_family = AF_INET;
    client.sin_port = htons(clientport);

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

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }

    // string ip = argv[1];
    uint16_t port = atoi(argv[1]);

    unique_ptr<udpServer> udpSV(new udpServer(handlerMessage, port)); 
    udpSV->initServer();
    udpSV->start();
    return 0;
}

效果展示:

windows与linux通信目的达成。

TCP

做了几个udp的实验,应该对网络接口更加能够理解了,接下来我们进入TCP。

demo1

先熟悉一下新接口和新写法,我们这个只实现一个单发单收的实验,只允许一发一收,不支持多客户端,多客户端只有一个能发消息,其他只能阻塞。

先展示代码,再说细节

tcpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "logMessage.hpp"

#define NUM 1024

namespace TCPServer
{
    using namespace std;

    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    static const uint16_t g_port = 8080;
    static const int g_backlog = 5;

    class TcpServer
    {
    public:
        TcpServer(const uint16_t &port = g_port)
            : _sockfd(-1), _port(port)
        {
        }

        void InitServer()
        {
            // 1.创建套接字
            _sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP  SOCK_STREAM
            if (_sockfd < 0)
            {
                logMessage(FATAL, "CREATE SOCKET ERROR");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "CREATE SOCKET SUCCESS");

            // 2.bind
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(_port);

            if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "BIND SOCKET ERROR");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "BIND SOCKET SUCCESS");

            // 3.设置socket为监听状态
            if (listen(_sockfd, g_backlog) < 0)
            {
                logMessage(FATAL, "LISTEN SOCKET ERROR");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "LISTEN SOCKET SUCCESS");
        }

        void start()
        {
            // 4.获取新链接
            while (true)
            {
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);
                int sock = accept(_sockfd, (struct sockaddr *)&peer, &peer_len);
                if (sock < 0)
                {
                    logMessage(ERROR, "ACCEPT ERROR, NEXT");
                    continue; // 这里的日志等级是ERROR,只是链接这个失败,不代表不继续接收其他链接
                }
                logMessage(NORMAL, "ACCEPT A NEW LINK SUCCESS");
                cout << "sock_fd : " << sock << endl;

                // 5.未来通信用的就是这个sock,面向字节流,后续全部都是文件操作
                
                // version 1  只允许一发一收 不支持多客户端  多客户端只有一个能发消息 其他只能阻塞
                serviceIO(sock);
                close(sock); // 对一个已经使用完毕的sock,一定要关闭这个sock,否则会导致文件描述符泄露
            }
        }

        void serviceIO(int sock)
        {
            char buffer[NUM];
            while (true)
            {
                ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
                if (s > 0) // 读到数据
                {
                    buffer[s] = 0;
                    cout << "recv message : " << buffer << endl;

                    string outbuffer = buffer;
                    outbuffer += "  server [echo] ";

                    // 写入文件 回复到客户端
                    write(sock, outbuffer.c_str(), sizeof(outbuffer));
                }

                else if (s == 0)
                {
                    logMessage(NORMAL, "CLIENT QUIT, SERVER QUIT");
                    break;
                }
            }
        }

        ~TcpServer()
        {
        }

    private:
        int _sockfd; // 不是用来进行数据通信的,用来监听链接到来,获取新连接的
        uint16_t _port;
    };
}

tcpServer.cc

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

using namespace std;
using namespace TCPServer;

static void Usage(string proc)
{
    cout << "Usage : " << proc << "  local_port" << endl;
    exit(USAGE_ERR);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }

    uint16_t port = atoi(argv[1]);

    unique_ptr<TcpServer> tcpsv(new TcpServer());
    tcpsv->InitServer();
    tcpsv->start();

    return 0;
}

tcpClient.hpp

#pragma once

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

namespace TCPClient
{
    using namespace std;

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

        }

        void InitClient()
        {
            _sockfd = socket(AF_INET,SOCK_STREAM,0);
            if (_sockfd < 0)
            {
                cout<<"CREATE SOCKET ERROR"<<endl;
                exit(2);
            }
        }

        void start()
        {
            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);

            //链接
            if(connect(_sockfd,(struct sockaddr*)&server,sizeof(server)) != 0)
            {
                cerr<<"socket connect error"<<endl;
            }

            else
            {
                string message;
                while(true)
                {
                    cout<<"Enter #";
                    getline(cin,message);
                    write(_sockfd,message.c_str(),sizeof(message));

                    char buffer[1024];
                    int n = read(_sockfd,buffer,sizeof(buffer) - 1);
                    if(n > 0)
                    {
                        buffer[n] = 0;
                        cout<<"Server return >>  "<<buffer<<endl;
                    }

                    else
                    {
                        break;
                    }
                }
            }
        }

        ~TcpClient()
        {
            if(_sockfd >= 0)
            {
                close(_sockfd);
            }
        }
    private:
        int _sockfd;
        uint16_t _serverport;
        string _serverip;
    };
}

tcpClient.cc

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

using namespace TCPClient;
using namespace std;

static void Usage(string proc)
{
    cerr<<"Usage : "<<proc<<"  server_ip  server_port"<<endl;
    exit(1);
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
    }

    string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    unique_ptr<TcpClient> tcpCLI(new TcpClient(ip,port));

    tcpCLI->InitClient();
    tcpCLI->start();
    
    return 0;
}

logMessage.hpp 设置日志

#pragma once

#include <iostream>
#include <string>
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <fstream>

// 日志等级
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

#define NUM 1024
#define LOG_PATH "./log.txt"

using namespace std;

const char *to_lever_str(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return nullptr;
    }
}

//可变参数列表为了可以让我们函数传参整这个格式
//logMessage(NOMAL,"message %d : %s",num,message)
void logMessage(int level, const char *message, ...) // 可变参数列表
{
    //time
    time_t t;
    time(&t);
    struct tm *lt = localtime(&t);
    char buffer[NUM];
    snprintf(buffer, sizeof(buffer), "%d.%d.%d %d:%d:%d", lt->tm_year + 1900, lt->tm_mon + 1, lt->tm_mday, lt->tm_hour, lt->tm_min, lt->tm_sec);

    //[level][time][pid][message]
    // 前文
    char logprefix[NUM];
    snprintf(logprefix, sizeof(logprefix), "[%s][%s][pid : %d]", to_lever_str(level), buffer, getpid());

    // 正文
    va_list arg;//定义一个va_list 实际上就是一个char* 
    va_start(arg, message);//让arg指向第一个参数列表的参数,会自动遍历参数列表
    char logcontent[NUM]; // message
    vsnprintf(logcontent, sizeof(logcontent), message, arg);
    //这里的message和arg 就相当于"message内容 %d" ,arg   举个例子后面的arg就是那个%d

    //写入文件
    FILE* fp = fopen(LOG_PATH,"a");
    if(fp == nullptr)
    {
        cout<<"open file failed"<<endl;
        exit(0);
    }

    fprintf(fp,"%s %s\n",logprefix,logcontent);
    fclose(fp);
}
细节问题
步骤问题

服务端步骤:

  1. socket创建套接字,这个套接字用来设置监听的套接字
  2. bind绑定ip和port
  3. listen设置监听状态
  4. accept接收新链接,返回一个套接字,这个套接字用来通信
  5. 接下来是文件操作,read,write

前两步可以看到跟udp是一样的,但是有一处不一样的是,sockert创建出的套接字是用来第三步设置监听状态的。第四步accept返回出的套接字才是用来通信。

客户端步骤:

  1.  创建socket
  2.  连接connect
  3. 文件操作

 这里可以看到不论服务端还是客户端到最后都转变为文件操作,最后的操作类似于管道。

日志数据:

设置日志级别:

  • DEBUG  
  • NORMAL  正常执行
  • WARNING   警告
  • ERROR    错误但不致命,不需退出,例如有若干个客户端来发起连接,accept接收第一个失败,不代表不接收其他的客户端。
  • FATAL   致命错误,程序退出。

解释现象

我们将服务端启动后,客户端启动服务端会显示出sock_fd : 4,表示打开4号文件描述符。

我们将客户端退出后,再次启动一个新的客户端,可以看到文件描述符还是4号,这可以证明我们对文件描述符进行关闭,我们客户端退出,操作系统会回收文件描述符。 对一个已经使用完毕的sock,一定要关闭这个sock,否则会导致文件描述符泄露。

                        

demo2

 设计一个多进程的版本,需要修改的代码只在tcpServer.hpp中的start()函数,细节很多,我们只拿那一个函数出来谈。

这份代码分成两种写法,下面这一种用信号,思路比较简单。

版本1
void start()
        {
            // 4.获取新链接
            signal(SIGCHLD,SIG_IGN);//在这种方式下,子进程状态信息会被丢弃,也就是自动回了,所以不会产生僵尸进程
            while (true)
            {
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);
                int sock = accept(_sockfd, (struct sockaddr *)&peer, &peer_len);
                if (sock < 0)
                {
                    logMessage(ERROR, "ACCEPT ERROR, NEXT");
                    continue; // 这里的日志等级是ERROR,只是链接这个失败,不代表不继续接收其他链接
                }
                logMessage(NORMAL, "ACCEPT A NEW LINK SUCCESS");
                cout << "sock_fd : " << sock << endl;

                // 5.未来通信用的就是这个sock,面向字节流,后续全部都是文件操作

                // version 2  多进程版本
                pid_t id = fork(); // 创建子进程
                if (id == 0)       // child
                {
                    close(_sockfd);
                    
                    serviceIO(sock);//在子进程进行服务操作
                    close(sock); // 对一个已经使用完毕的sock,一定要关闭这个sock,否则会导致文件描述符泄露
                    exit(0);
                }

                close(sock);//一定要关闭
            }
        }
代码解释

        最开始调用signal函数,将SIGCHID设置为忽略SIG_IGN,在这种设置方式下,子进程的状态信息会被丢弃,也就是自动回收,所以不会产生僵尸进程。

        接下来的代码跟版本一是一样的,直到version2那里,调用fork创建子进程,使父子进程各做各的事,子进程只做服务操作serviceIO函数操作,不需要监听,则在子进程中关闭监听的文件描述符_sockfd,最后再关闭用来通信的文件描述符,对一个已经使用完毕的sock,一定要关闭这个sock,否则会导致文件描述符泄露。

        父进程这里也一定要关闭文件描述符,原因也是一样,一定要关!!!!下面可以看一下反面教材,没关的效果。可以看到客户端每连接一次,文件描述符就加一个。如果我们服务端一直启动让客户端连,总有一天文件描述符会不够。

版本二

这个方法的思路非常巧妙,不过可能比较难理解。

        void start()
        {
            // 4.获取新链接
            while (true)
            {
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);
                int sock = accept(_sockfd, (struct sockaddr *)&peer, &peer_len);
                if (sock < 0)
                {
                    logMessage(ERROR, "ACCEPT ERROR, NEXT");
                    continue; // 这里的日志等级是ERROR,只是链接这个失败,不代表不继续接收其他链接
                }
                logMessage(NORMAL, "ACCEPT A NEW LINK SUCCESS");
                cout << "sock_fd : " << sock << endl;

                // 5.未来通信用的就是这个sock,面向字节流,后续全部都是文件操作

                // version 2  多进程版本
                pid_t id = fork(); // 创建子进程
                if (id == 0)       // child
                {
                    close(_sockfd);//子进程不需要监听,将不需要的文件描述符关闭,以防误操作
                    if(fork() > 0)//创建孙子进程成功后子进程直接退出
                       exit(0);
                    serviceIO(sock);//在子进程进行服务操作
                    close(sock); // 对一个已经使用完毕的sock,一定要关闭这个sock,否则会导致文件描述符泄露
                    exit(0);
                }

                //father
                   pid_t ret = waitpid(id,nullptr,0);//回收子进程
                   if(ret > 0)
                   {
                      cout<<"wait success "<<ret<<endl;
                   }

                close(sock);//一定要关闭
               
            }
代码解释

        也是多进程,所以创建子进程,让父子进程各做各的事, 但这里有一个问题,如果我们不用信号忽略SIGCHID,我们父进程就得等待,回收子进程。如果是正常的阻塞等待,那跟demo1就没有任何区别,都是单一个发单一个收,得不到效果。如果这里使用非阻塞等待,非阻塞等待会导致父进程一直创建子进程。这里只能使用阻塞等待。

        细节来了,在子进程中创建一个子进程,也就是孙子进程,只要孙子进程一创建成功,子进程立刻退出,使孙子进程成为孤儿进程,操作系统就要领养这个孤儿进程,后续的回收都由操作系统做。注意一点,进程是各管各自的孩子的,所以孙子进程不归父进程管。这里使用孙子进程去执行服务操作。

demo3 

设计一个多线程版本,代码改动较大,但只在tcpServer.hpp中修改。

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include "logMessage.hpp"

#define NUM 1024

namespace TCPServer
{
    using namespace std;

    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    static const uint16_t g_port = 8080;
    static const int g_backlog = 5;
    class TcpServer;

    class ThreadData
    {
    public:
        ThreadData(TcpServer* self,int sock)
        :_this(self)
        ,_sock(sock)
        {

        }
    public:
        TcpServer* _this;
        int _sock;
    };

    class TcpServer
    {
    public:
        TcpServer(const uint16_t &port = g_port)
            : _sockfd(-1), _port(port)
        {
        }

        void InitServer()
        {
            // 1.创建套接字
            _sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP  SOCK_STREAM
            if (_sockfd < 0)
            {
                logMessage(FATAL, "CREATE SOCKET ERROR");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "CREATE SOCKET SUCCESS");

            // 2.bind
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(_port);

            if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "BIND SOCKET ERROR");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "BIND SOCKET SUCCESS");

            // 3.设置socket为监听状态
            if (listen(_sockfd, g_backlog) < 0)
            {
                logMessage(FATAL, "LISTEN SOCKET ERROR");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "LISTEN SOCKET SUCCESS");
        }

        void start()
        {
            // 4.获取新链接
            while (true)
            {
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);
                int sock = accept(_sockfd, (struct sockaddr *)&peer, &peer_len);
                if (sock < 0)
                {
                    logMessage(ERROR, "ACCEPT ERROR, NEXT");
                    continue; // 这里的日志等级是ERROR,只是链接这个失败,不代表不继续接收其他链接
                }
                logMessage(NORMAL, "ACCEPT A NEW LINK SUCCESS");
                cout << "sock_fd : " << sock << endl;

                // 5.未来通信用的就是这个sock,面向字节流,后续全部都是文件操作
                
                // version 3  多线程
                pthread_t tid;
                ThreadData* td = new ThreadData(this,sock);
                pthread_create(&tid,nullptr,threadRoutine,td);
            }
        }

        static void* threadRoutine(void* args)
        {
            pthread_detach(pthread_self());//去关联主线程不用等待join
            ThreadData* td = static_cast<ThreadData*>(args);
            td->_this->serviceIO(td->_sock);
            close(td->_sock);
            delete td;
            return nullptr;
        }

        void serviceIO(int sock)
        {
            char buffer[NUM];
            while (true)
            {
                ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
                if (s > 0) // 读到数据
                {
                    buffer[s] = 0;
                    cout << "recv message : " << buffer << endl;

                    string outbuffer = buffer;
                    outbuffer += "  server [echo] ";

                    // 写入文件 回复到客户端
                    write(sock, outbuffer.c_str(), sizeof(outbuffer));
                }

                else if (s == 0)
                {
                    logMessage(NORMAL, "CLIENT QUIT, SERVER QUIT");
                    break;
                }
            }
        }

        ~TcpServer()
        {
        }

    private:
        int _sockfd; // 不是用来进行数据通信的,用来监听链接到来,获取新连接的
        uint16_t _port;
    };
}
代码解释

        多线程的创建,调用pthread_create,这里的细节在后两个参数,倒数第三个参数传一个函数,让新线程去执行,而且在类内这个函数必须是static,因为static函数没有this指针,不用static会报错,但static函数我们又没办法调用类内函数,没有this指针。

        我们做一个设计,定义一个ThreadData类,里面成员变量有一个TcpServer的对象,还有一个用来通信的文件描述符。传参构造自然就传入的就是TcpServer的this指针和accept返回的sock。然后在第四个参数中直接传入对象。这样我们就可以达到我们想要的效果,在static函数内调用类内函数,也就是serviceIO。

        因为主线程正常也需要等待新线程,如果不想主线程等待,可以调用pthread_detach()去关联,这样主线程就不需要调用pthread_join。

demo4

重头戏来了,代码的最终版本,线程池版本,因为这里的代码改动太大,并且代码篇幅太长,TCP_ThreadPool点击进入查看代码。

代码解释

这个代码加入了Mutex.hpp用于加锁,Thread.hpp与ThreadPool线程与线程池,Task.hpp分发任务,daemo.hpp守护进程这里重点讲这一个。

守护进程

        也叫做精灵进程,真实的情况下,服务端不应该收到客户端退出或者登录而影响,启动时会创建会话,退出时,会清理会话。这样的任务,可能会受到用户的登录或者注销的影响。为了不受影响,自成会话,自成进程组和终端设备无关,这样的进程叫做守护进程。

引入一个新接口setsid

#include <uinstd.h>

pid_t setsid(void); 

功能:哪一个进程调用就可以自建一个会话,自己是组长。

注意:如果该进程已经是本会话的组长(进程组的第一个进程)。不能调用该函数。

成功返回该进程的pid。

补充linux的前后台转换的指令

fg : 将后台作业带到前台执行。

bg : 将前台作业带去后台执行。

jobs : 列出当前任务。

可执行文件 & : 将改执行文件放到后台。

ctrl + z : 暂停并放到后台。

注意:有且只有一个前台。

其他代码可以点进连接查看,这里只拿出daemo.hpp来谈一谈细节。

#pragma once

#include <unistd.h>
#include <signal.h>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "logMessage.hpp"

#define DEV "/dev/null" // 类似回收站,只要写入到里面的数据全部丢弃

// 守护进程
void daemon_self()
{
    // 1.让调用进程忽略掉异常的信号  以防客户端退出,而服务端还在继续写入,导致崩溃退出
    signal(SIGPIPE, SIG_IGN);

    // 2.使自己不是本进程组的组长
    if (fork() > 0)
        exit(2);

    // 3.使其成为新会话组的组长 子进程 ---- 守护进程 精灵进程,本质就是孤儿进程的一种
    pid_t id = setsid();
    if (id < 0)
    {
        logMessage(FATAL, "SETSID ERROR");
        exit(2);
    }

    // 4.处理文件描述符,因为守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
    int fd = open(DEV,O_RDWR);
    if(fd >= 0)
    {
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
        close(fd);
    }

    else
    {
        logMessage(FATAL,"OPEN ERROR");
        exit(1);
    }
}

步骤说明:

  1. 让调用的进程忽略掉异常的信号,以防客户端已经退出,而服务端还在写入,导致崩溃退出。signal(SIGPIPE,SIG_IGN);
  2. 要调用setsid,事先必须使该进程不能成为它当前进程组的组长。如果创建子进程成功,父进程立马退出。则该进程就不是当前进程组的组长。
  3. 再调用setsid自成新会话的组长。
  4. 处理文件描述符,因为守护进程是脱离终端的,所以应该关闭或者重定向之前默认打开的文件描述符。/dev/null是一个类似回收站的东西,向里面写入的数据全部会被丢弃。将之前默认打开的文件描述符,重定向到/dev/null中。

        运行效果,可以看到服务端启动起来后是没有反应的,因为他是脱离终端,但看红框里的内容,服务器确确实实的启动了,并且客户端也可以连接和发送消息。

        至此整篇文章结束,相信你看到这里一定收获满满,非常感谢您的观看!!        

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值