【Linux】网络编程套接字(C++)

news2024/11/27 13:49:17

目录

一、预备知识

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

【1.2】认识端口号

【1.3】理解 "端口号" 和 "进程ID"

【1.4】理解源端口号和目的端口号

【1.5】认识TCP协议

【1.6】认识UDP协议

二、网络字节序

【2.1】socket编程接口

【2.1.1】socket API

【2.1.2】bind API

【2.1.3】listen API

【2.1.4】accept API

【2.1.5】connect API

【2.1.6】recvfrom API

【2.1.7】sendto API

【2.2】端口转换函数

【2.3】地址转换函数

【2.3】sockaddr结构

【2.3.1】sockaddr 结构

【2.3.2】sockaddr_in 结构

【2.3.3】sockaddr结构专用初始化函数

三、netstat 查看系统网络

【5】UDP实现网络通信

【5.1】Makefile文件代码

【5.2】UdpServer文件代码

【5.2.1】UdpServer.hpp文件代码

【5.2.2】UdpServer.cc文件代码

【5.3】UdpClient文件代码

【5.3.1】UdpClient.hpp文件代码

【5.3.2】UdpClient.cc文件代码

【7】TCP实现网络通信

【7.1】Makefile文件代码

【7.2】Log.hcc文件代码

【7.3】TcpServer文件代码

【7.3.1】TcpServer.hpp文件代码

【7.3.2】TcpServer.cc文件代码

【7.4】TcpClient文件代码

【7.4.1】TcpClient.hpp文件代码

【7.4.2】TcpClient.cc文件代码

【9】TCP协议通讯流程

【10】TCP 和 UDP 对比


Linux网络编程套接字(C++)

一、预备知识

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

        在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。

【思考】 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析。

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

【1.2】认识端口号

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

  • 端口号是一个2字节16位的整数。

  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。

  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。

  • 一个端口号只能被一个进程占用。

ip地址(主机全网唯一性) + 该主机上的端口号,标识服务器上进程的唯一性(ip + PortA ,ip + PortB),网络通信的本质:其实就是进程与进程间的通信,ip保证(全网唯一),port保证(主机唯一)。

【1.3】理解 "端口号" 和 "进程ID"

        我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。

【1.4】理解源端口号和目的端口号

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

【1.5】认识TCP协议

        此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。

  • 传输层协议

  • 有连接

  • 可靠传输

  • 面向字节流

【1.6】认识UDP协议

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

  • 传输层协议

  • 无连接

  • 不可靠传输

  • 面向数据报

二、网络字节序

【2.1】socket编程接口

#include <socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// domain : 域:本地通信、网络通信
// type   : 我们socket提供的能力类型
#include <socket.h>
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
 socklen_t address_len);
// socket : 绑定指定的文件描述符
// sockaddr : 参数结构
#include <sys/types.h> 
#include <sys/socket.h>
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
#include <sys/types.h> 
#include <sys/socket.h>
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
#include <sys/types.h> 
#include <sys/socket.h>
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
#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);
#include <sys/types.h>
#include <sys/socket.h>
// 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

【2.1.1】socket API

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。

  • 应用程序可以像读写文件一样用read/write在网络上收发数据。

  • 如果socket()调用出错则返回-1。 对于IPv4, family参数指定为AF_INET。

  • 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议。

  • protocol参数的介绍从略,指定为0即可。

【2.1.2】bind API

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号。

  • bind()成功返回0,失败返回-1。

  • bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号。

  • 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

【2.1.3】listen API

#include <sys/socket.h>
int listen(int socket, int backlog);
  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究。

  • listen()成功返回0,失败返回-1。

【2.1.4】accept API

#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
  • 三次握手完成后, 服务器调用accept()接受连接。

  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号; 如果给addr 参数传NULL,表示不关心客户端的地址。

  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);。

【2.1.5】connect API

#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
  • 客户端需要调用connect()连接服务器。

  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址。

  • connect()成功返回0,出错返回-1。

【2.1.6】recvfrom API

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:文件描述符(套接字)的网络端口

  • buf:读取到缓冲区

  • len:缓冲区长度

  • flags:读取方式默认为0(阻塞式读取)

  • src_addr:读取到的套接字信息

  • addrlen:套接字长度

【2.1.7】sendto API

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:文件描述符(套接字)的网络端口

  • buf:发送的缓冲区

  • len:缓冲区长度

  • flags:发送方式默认为0(阻塞式发送)

  • dest_addr:自己的套接字信息发送出去

  • addrlen:套接字长度

【2.2】端口转换函数

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。

  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。

  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。

  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。

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

#include <arpa/inet.h>
// 函数将无符号整数hostlong从主机字节顺序转换为网络字节顺序.
uint32_t htonl(uint32_t, hostlong);
#include <arpa/inet.h>
// 函数将无符号短整数hostshort从主机字节顺序转换为网络字节顺序.
uint16_t htons(uint16_t, hostshort);
#include <arpa/inet.h>
// 函数将无符号整数netlong从网络字节顺序转换为主机字节顺序.
uint32_t ntohl(uint32_t netlong);
#include <arpa/inet.h>
// 函数将无符号短整数netshort从网络字节顺序转换为主机字节顺序.
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。

  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

【2.3】地址转换函数

        本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换

        inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - Internet地址操作例程

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

int inet_aton(const char *cp, struct in_addr *inp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// inet_addr()函数的作用是:将Internet主机地址cp从IPv4的数字点法转换为网络字节顺序的二进制数据。如果输入无效,INADDR_NONE(通常是-1)返回。使用这个函数是有问题的,因为-1是一个有效的地址(255.255.255.255)。避免使用inet_aton()、inet_pton(3)或getad‐Drinfo(3)提供了一种更清晰的方式来指示错误返回。
in_addr_t inet_addr(const char *cp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_network(const char *cp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// net_ntoa()函数的作用是:将Internet主机地址(以网络字节顺序给出)转换为IPv4点分十进制格式的字符串。字符串以静态方式返回已分配的缓冲区,后续调用将覆盖该缓冲区。
char *inet_ntoa(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

struct in_addr inet_makeaddr(int net, int host);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_lnaof(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_netof(struct in_addr in);

        其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。

【2.3】sockaddr结构

        socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。

        IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。

        IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。

        socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

【2.3.1】sockaddr 结构

        sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:

struct sockaddr {  
     sa_family_t sin_family;	  //地址族
    char sa_data[14]; 			 //14字节,包含套接字中的目标地址和端口信息               
}; 

【2.3.2】sockaddr_in 结构

        sockaddr_in在头文件#include<netinet/in.h>#include <arpa/inet.h>`中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:

struct sockaddr_in {
    sa_family_t			sin_family; 	// 地址族(Address Family)
    uint16_t			sin_port;		// 16位TCP\UDP端口号
    struct in_addr		sin_addr;		// 32位ip地址
    char				sin_zero[8]		// 不使用
}

// 该结构体中提到另一个结构体in_addr定义如下:它用来存放32位ip地址
struct in_addr {
    in_addr_t			s_addr;			// 32位IPv4地址
}

【2.3.3】sockaddr结构专用初始化函数

#include <strings.h>
// 对struct sockaddr 数据类型做初始化            
// bzero()函数将从s开始的区域的前n个字节设置为零(包含'\0'的字节)。
void bzero(void *s, size_t n);

三、netstat 查看系统网络

语法

netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]

功能

        Linux netstat 命令用于显示网络状态。

        利用 netstat 指令可让你得知整个 Linux 系统的网络情况。

选项

  • -a或--all 显示所有连线中的Socket。

  • -A<网络类型>或--<网络类型> 列出该网络类型连线中的相关地址。

  • -c或--continuous 持续列出网络状态。

  • -C或--cache 显示路由器配置的快取信息。

  • -e或--extend 显示网络其他相关信息。

  • -F或--fib 显示路由缓存。

  • -g或--groups 显示多重广播功能群组组员名单。

  • -h或--help 在线帮助。

  • -i或--interfaces 显示网络界面信息表单。

  • -l或--listening 显示监控中的服务器的Socket。

  • -M或--masquerade 显示伪装的网络连线。

  • -n或--numeric 直接使用IP地址,而不通过域名服务器。

  • -N或--netlink或--symbolic 显示网络硬件外围设备的符号连接名称。

  • -o或--timers 显示计时器。

  • -p或--programs 显示正在使用Socket的程序识别码和程序名称。

  • -r或--route 显示Routing Table。

  • -s或--statistics 显示网络工作信息统计表。

  • -t或--tcp 显示TCP传输协议的连线状况。

  • -u或--udp 显示UDP传输协议的连线状况。

  • -v或--verbose 显示指令执行过程。

  • -V或--version 显示版本信息。

  • -w或--raw 显示RAW传输协议的连线状况。

  • -x或--unix 此参数的效果和指定"-A unix"参数相同。

  • --ip或--inet 此参数的效果和指定"-A inet"参数相同。

【备注】如果想要看到更详细的添加sudo

[shaxiang@VM-8-14-centos 99_Lesson_20230707_CodeExecute_UdpNetWork_Linux]$ netstat -nuap // 监控端口号和IP
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -                   
udp        0      0 10.0.8.14:123           0.0.0.0:*                           -                   
udp        0      0 127.0.0.1:123           0.0.0.0:*                           -                   
udp6       0      0 fe80::5054:ff:fec6::123 :::*                                -                   
udp6       0      0 ::1:123                 :::*                                -             
[shaxiang@VM-8-14-centos 20230811_TcpNewWork]$ netstat -nltp	// 监控TCP服务器
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:45638         0.0.0.0:*               LISTEN      20145/node          
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      4410/./TcpServer    
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::3306                 :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                       

【5】UDP实现网络通信

【5.1】Makefile文件代码

# 定义变量并且赋值相应的字符串信息
cc := g++
standard := -std=c++11

compile: udpServer udpClient
udpServer: UdpServer.cc 
	$(cc) -o $@ $^ $(standard)
udpClient: UdpClient.cc 
	$(cc) -o $@ $^ $(standard)

clean:
	rm -rf udpServer udpClient

# .PHONY: 可以避免与系统的命令冲突
.PHONY: compile clean 

【5.2】UdpServer文件代码

【5.2.1】UdpServer.hpp文件代码

#pragma once
/* C头文件包含 */
#include <cstdlib>
#include <cstring>
#include <cerrno>

/* C++头文件包含 */
#include <iostream>
#include <functional>
#include <string>

/* 系统头文件包含 */
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/* UDP服务器Demo封装命名空间 */
namespace Server
{
    enum { USAGE_ERR = 1, SOCKET_ERR = 2, BIND_ERR = 3 };
    const int g_num = 1024;


    /* UDP服务器命名空间 */
    class UdpServer
    {
    private:
        using func_t = std::function<void(const int&, const std::string&, const uint16_t&, const std::string&)>;

    public:
        /* 构造函数 */
        UdpServer(const func_t& func, const uint16_t& port)
            : _selfSockFd(-1)
            , _selfSockProt(port)
            , _callBack(func)
        {}

        /* 析构函数 */
        ~UdpServer() 
        {}

    public:
        /* 初始化 */
        void Init()
        {
            // 创建socket(打开网卡驱动文件)
            _selfSockFd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字    
            if(_selfSockFd < 0)
            {
                std::cerr << "socket error " << errno << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "socket success..." << std::endl;

            // 绑定自己的Ip和端口号
            struct sockaddr_in localAddr; // 套接字对象
            bzero(&localAddr, sizeof(localAddr)); // 初始化
            localAddr.sin_family = AF_INET; // 地址家族
            localAddr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
            localAddr.sin_port = htons(_selfSockProt); // 端口号
            
            int bindState = bind(_selfSockFd, (struct sockaddr*)& localAddr, sizeof(localAddr)); // 绑定套接字
            if(bindState < 0)
            {
                std::cerr << "bind error " << errno << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind success..." << std::endl;
        }

        /* 启动服务 */
        void Start()
        {
            // 启动
            char reBuffer[g_num] = "\0";
            while(true)
            {
                struct sockaddr_in clientAddr; // 套接字对象
                bzero(&clientAddr, sizeof(clientAddr)); // 初始化
                socklen_t addrLength = sizeof(clientAddr);
                // 读取
                int reCnt = recvfrom(_selfSockFd, reBuffer, sizeof(reBuffer) - 1, 0, (struct sockaddr*)& clientAddr, &addrLength); 
                if(reCnt > 0)
                {
                    reBuffer[reCnt] = '\0'; // 处理字符串
                    std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 获取客户端ip
                    uint16_t clientPort = ntohs(clientAddr.sin_port); // 获取客户端port
                    // 调用回调函数
                    _callBack(_selfSockFd, clientIp, clientPort, reBuffer);
                }
            }
        }

    private:
        uint16_t     _selfSockProt;     // 服务器(自己)UDP通讯端口号
        int          _selfSockFd;       // 服务器(自己)UDP通讯文件描述符   
        func_t       _callBack;         // 服务器(自己)UDP回调函数
    };
};

【5.2.2】UdpServer.cc文件代码

#include <memory>
#include "UdpServer.hpp"
using namespace Server;

/* 函数接口:用户启动提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}

/* 函数回调:通讯功能 */
void CallbackFunction(const int& selfSockFd, const std::string& clientIp, const uint16_t& clientPort, const std::string& message) 
{
    // 打印接收信息
    std::cout << "client ip[" << clientIp << "] " << "port[" << clientPort << "]: " << message << std::endl;
}

/* 函数接口:程序入口 */
int main(int argc, char** argv)
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 服务器对象交管给智能指针
    uint16_t userPort = atoi(argv[1]);
    std::unique_ptr<UdpServer> udpSevr(new UdpServer(CallbackFunction, userPort));
    udpSevr->Init();
    udpSevr->Start();

    return 0; 
}

【5.3】UdpClient文件代码

【5.3.1】UdpClient.hpp文件代码

#pragma once
/* C头文件包含 */
#include <string>
#include <cstring>

/* C++头文件包含 */
#include <iostream>

/* 系统头文件包含 */
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/* UDP客户端Demo封装命名空间 */
namespace Client
{
    enum { USAGE_ERR = 1, SOCKET_ERR = 2, BIND_ERR = 3 };
    const int g_num = 1024;

    /* UDP客户端命名空间 */
    class UdpClient
    {
    public:
        /* 构造函数 */
        UdpClient(const uint16_t& port, const std::string& ip)
            : _selfSockFd(-1)
            , _sevrSockIp(ip)
            , _sevrSockProt(port)
        {}

        /* 析构函数 */
        ~UdpClient() 
        {}

    public:
        /* 初始化 */
        void Init()
        {   
            // 创建socket(打开网卡驱动文件)
            _selfSockFd = socket(AF_INET, SOCK_DGRAM, 0);    
            if(_selfSockFd < 0)
            {
                std::cerr << "socket error " << errno << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "socket success..." << std::endl;
        }

        /* 启动服务 */
        void Start()
        {
            struct sockaddr_in serverAddr; // 套接字对象
            bzero(&serverAddr, sizeof(serverAddr)); // 初始化
            serverAddr.sin_family = AF_INET; // 地址家族
            serverAddr.sin_addr.s_addr = inet_addr(_sevrSockIp.c_str()); // 服务器ip
            serverAddr.sin_port = htons(_sevrSockProt); // 服务器port

            // 启动
            char stoBuffer[g_num];
            while(true)
            {   
                std::cout << "Please Say: ";
                std::cin.getline(stoBuffer, g_num);  
                // 发送信息
                sendto(_selfSockFd, stoBuffer, sizeof(stoBuffer), 0, (struct sockaddr*)& serverAddr, sizeof(serverAddr));

            }
        }

    private:
        std::string  _sevrSockIp;       // 服务器(对方)UDP通讯Ip地址
        uint16_t     _sevrSockProt;     // 服务器(对方)UDP通讯端口号
        int          _selfSockFd;       // 客户端(自己)UDP通讯文件描述符   
    };
};

【5.3.2】UdpClient.cc文件代码

#include <memory>
#include "UdpClient.hpp"
using namespace Client;

/* 函数接口:用户启动提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " ServerIp" << " ServerPort\n\n" << std::endl;
}

/* 函数接口:程序入口 */
int main(int argc, char** argv)
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 客户端对象交管给智能指针
    uint16_t userPort = atoi(argv[2]);
    std::string userIp = argv[1];
    std::unique_ptr<UdpClient> udpClit(new UdpClient(userPort, userIp));
    udpClit->Init();
    udpClit->Start();

    return 0; 
}

【7】TCP实现网络通信

【7.1】Makefile文件代码

# 定义变量并且赋值相应的字符串信息
cc := g++
standard := -std=c++11

compile:tcpServer tcpClient
tcpServer:TcpServer.cc
	$(cc) -o $@ $^ $(standard)
tcpClient:TcpClient.cc
	$(cc) -o $@ $^ $(standard)

clean:
	rm -rf tcpServer tcpClient
	
# .PHONY: 可以避免与系统的命令冲突
.PHONY:clean compile

【7.2】Log.hcc文件代码

#pragma once 
#include <iostream>

#define DEBUG    0 // 调试等级
#define NORMAL   1 // 正常等级
#define WARNING  2 // 警告等级
#define ERROR    3 // 错误等级
#define FATAL    4 // 致命等级

/* 函数:日志等级转为字符串 */
void LevelToString(const int& level)
{
    
}

/* 函数:日志打印 */
void LogMessage(const int& level, const std::string& message)
{
    // 格式:[日志等级] [时间戳/时间] [pid] [信息]
    // 比如:[WARNING] [2023-05-11 18:09:23] [12345] [创建socket文件描述符失败!]    
    std::cout << message << std::endl;
}

【7.3】TcpServer文件代码

【7.3.1】TcpServer.hpp文件代码

#pragma once
#include <iostream>

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

#include "Log.hpp"

namespace Server
{
    /* 枚举常量 */
    enum{ 
        USAGE_ERR = 1, 
        SOCKET_ERR, 
        BIND_ERR,  
        LISTEN_ERR,
        ACCEPT_ERR
    };
    const int g_backLog = 10;
    const int g_num = 1024;

    class TcpServer
    {
    public:
        /* 构造函数 */
        TcpServer(const uint16_t& port)
            : _selfListenFd(-1)
            , _selfPort(port)
        {}

        /* 析构函数 */
        ~TcpServer() {}

    public:
        /* 初始化 */
        void Init()
        {
            // No.1 创建套socket文件套接字对象
            _selfListenFd = socket(AF_INET, SOCK_STREAM, 0);
            if(_selfListenFd < 0)
            {
                LogMessage(FATAL, "create socket fail!");
                exit(SOCKET_ERR);
            }
            LogMessage(NORMAL, "create socket success...");

            // No.2 绑定自己的网络信息
            struct sockaddr_in localAddr; // 创建sockAddr对象
            bzero(&localAddr, sizeof(localAddr)); // 初始化对象
            localAddr.sin_family = AF_INET; // 绑定协议家族
            localAddr.sin_addr.s_addr = INADDR_ANY; // 绑定回环地址[0.0.0.0]
            localAddr.sin_port = htons(_selfPort); // 绑定端口号
            int bindState = bind(_selfListenFd, (struct sockaddr*)&localAddr, sizeof(localAddr));
            if(bindState < 0)
            {
                LogMessage(FATAL, "bind socket fail!");
                exit(BIND_ERR); 
            }
            LogMessage(NORMAL, "bind socket success...");

            // No.3 开始监听网络
            int listenState = listen(_selfListenFd, g_backLog);
            if(listenState < 0)
            {
                LogMessage(FATAL, "listen socket fail!");
                exit(LISTEN_ERR); 
            }
            LogMessage(NORMAL, "listen socket success...");
        }

        /* 启动 */
        void Start()
        {
            // 运行服务器
            while(true)
            {
                // No.4 获取新连接
                struct sockaddr_in clientAddr; // sockAddr对象
                socklen_t addrLen = sizeof(clientAddr); // 求长度
                bzero(&clientAddr, sizeof(clientAddr)); // 初始化
                // 接收链接
                _selfSocketFd = accept(_selfListenFd, (struct sockaddr*)&clientAddr, &addrLen);
                if(_selfListenFd < 0)
                {
                    LogMessage(ERROR, "accept socket fail!");
                    exit(ACCEPT_ERR); 
                }
                LogMessage(NORMAL, "accept socket success...");
                std::cout << "listenFd: " << _selfListenFd << " " << "sockFd: " << _selfSocketFd << std::endl;

                // 面向字节流的读取(对文件进行读取)
                ServiceIO();
                close(_selfSocketFd); // 必须关闭,防止文件描述符泄露!
                break;
            }
        }

        /* 面向字节流读取消息 */
        void ServiceIO()
        {
            while(true)
            {
                // 接收
                char inBuffer[g_num] = { '\0' };
                ssize_t n = read(_selfSocketFd, inBuffer, sizeof(inBuffer) - 1);
                if(n > 0)
                {
                    // 处理读取到的内容
                    inBuffer[n] = '\n';
                    std::cout << "recv buffer: " << inBuffer << std::endl;

                    // 响应
                    std::string outBuffer;
                    outBuffer = "Server echo# ";
                    outBuffer += inBuffer;
                    write(_selfSocketFd, outBuffer.c_str(), outBuffer.size());
                }
                else if(n == 0) // 在读取的时候,如果读取到了0,说明客户端已经退出了,这时候服务器也可以退出了!
                {
                    LogMessage(NORMAL, "client quit me too...");
                    break;
                }
            }
        }

    private:
        int         _selfListenFd;  // TCP通讯(自己)网络监听文件描述符
        int         _selfSocketFd;  // TCP通讯(自己)网络服务文件描述符
        uint16_t    _selfPort;      // TCP通讯(自己)网络端口
    };
};

【7.3.2】TcpServer.cc文件代码

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

using namespace Server;

/* 函数:消息提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}

/* 函数:程序入口函数 */
int main(int argc, char** argv)
{
    // 检查用户启动命令
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 将服务器对象转交给智能指针进行管理
    // 获取用户指定的端口号
    uint16_t userPort = atoi(argv[1]);
    std::unique_ptr<TcpServer> pTcpSevr(new TcpServer(userPort));
    pTcpSevr->Init();
    pTcpSevr->Start();
    
    return 0;
}

【7.4】TcpClient文件代码

【7.4.1】TcpClient.hpp文件代码

#pragma once
#include <iostream>

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

#include "Log.hpp"

namespace Client
{
    /* 枚举常量 */
    enum{ 
        USAGE_ERR = 1, 
        SOCKET_ERR, 
        BIND_ERR,  
        LISTEN_ERR,
        ACCEPT_ERR,
        CONNECT_ERR
    };
    const int g_num = 1024;


    class TcpClient
    {
    public:
        /* 构造函数 */
        TcpClient(const std::string& sevrIp, const uint16_t& sevrPort)
            : _selfSocketFd(-1)
            , _sevrSocketIp(sevrIp)
            , _sevrSocketPort(sevrPort)
        {}
        
        /* 析构函数 */
        ~TcpClient() {}

    public:
        /* 初始化 */
        void Init() 
        {
            // No.1 创建套socket文件套接字对象
            _selfSocketFd = socket(AF_INET, SOCK_STREAM, 0);
            if(_selfSocketFd < 0)
            {
                LogMessage(FATAL, "create socket fail!");
                exit(SOCKET_ERR);
            }
            LogMessage(NORMAL, "create socket success...");

            // No.2 创建链接
            struct sockaddr_in local; // 创建sockAddr对象
            bzero(&local, sizeof(local)); // 初始化对象
            local.sin_family = AF_INET; // 绑定协议家族
            local.sin_addr.s_addr = inet_addr(_sevrSocketIp.c_str()); // 绑定服务器Ip
            local.sin_port = htons(_sevrSocketPort); // 绑定服务器端口号
            int connectState = connect(_selfSocketFd, (struct sockaddr*)&local, sizeof(local));
            if(connectState < 0)
            {
                LogMessage(FATAL, "connect socket fail!");
                exit(CONNECT_ERR);   
            }
            LogMessage(NORMAL, "connect socket success...");
        }

        /* 启动 */
        void Start()
        {
            // 运行
            while(true)
            {
                // 响应
                std::cout << "Plase Say# ";
                std::string outBuffer;
                std::getline(std::cin, outBuffer);
                write(_selfSocketFd, outBuffer.c_str(), outBuffer.size());

                // 等待回复
                char inBuffer[g_num] = { '\0' };
                int n = read(_selfSocketFd, inBuffer, sizeof(inBuffer) - 1);
                if(n > 0)
                {   
                    // 处理读取到的内容
                    inBuffer[n] = '\0';
                    std::cout << inBuffer << std::endl;
                }
                else if(n == 0) // 在读取的时候,如果读取到了0,说明服务器已经退出了,这时候客户端也可以退出了!
                {
                    LogMessage(NORMAL, "server quit me too...");
                    break;
                }
            }

            close(_selfSocketFd);
        }

    private:
        int             _selfSocketFd;
        std::string     _sevrSocketIp;
        uint16_t        _sevrSocketPort;
    };
};

【7.4.2】TcpClient.cc文件代码

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

using namespace Client;

/* 函数:消息提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}

/* 函数:程序入口函数 */
int main(int argc, char** argv)
{
    // 检查用户启动命令
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 将客户端对象转交给智能指针进行管理
    // 获取用户指定的IP地址和端口号
    std::string userIp = argv[1];
    uint16_t userPort = atoi(argv[2]);
    std::unique_ptr<TcpClient> pTcpClit(new TcpClient(userIp, userPort));
    pTcpClit->Init();
    pTcpClit->Start();
    
    return 0;
}

【9】TCP协议通讯流程

【服务器初始化】

  • 调用socket, 创建文件描述符。

  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失 败。

  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备。

  • 调用accecpt, 并阻塞, 等待客户端连接过来。

【建立连接的过程 】

  • 调用socket, 创建文件描述符。

  • 调用connect, 向服务器发起连接请求。

  • connect会发出SYN段并阻塞等待服务器应答; (第一次)。

  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)。

  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)。

这个建立连接的过程, 通常称为 三次握手

【数据传输的过程 】

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据。

  • 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待。

  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答。

  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求。

  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去。

【断开连接的过程】

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)。

  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)。

  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)。

  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次) 。

这个断开连接的过程, 通常称为四次挥手

在学习socket API时要注意应用程序和TCP协议层是如何交互的。

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段。

  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些 段,再比如read()返回0就表明收到了FIN段 。

【10】TCP 和 UDP 对比

可靠传输不可靠传输
TCP通讯UDP通讯
有链接无连接
字节流数据包

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1008770.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5款实用的Redis可视化工具

Redis可视化工具是一种用于管理和监视Redis数据库的工具&#xff0c;它提供了一个可视化界面来操作和查看Redis的数据和配置信息&#xff0c; 可以让我们更加直观地管理和操作Redis数据库。下面介绍四款比较不错的Redis可视化工具。 1.Redis可视化工具推荐—RedisInsight Red…

美创科技入选第二届安徽省网络和数据安全应急技术支撑单位

9月11日&#xff0c;2023年安徽省网络安全宣传周活动在阜阳市正式启动。安徽省委常委、宣传部部长陈舜出席并宣布网安周正式启动。阜阳市委书记刘玉杰、省委宣传部副部长、省委网信办主任张杰华出席并致辞。 开幕式上&#xff0c;省委网信办副主任齐海洋发布第二届安徽省网络和…

Template serialization - shared_ptr<class T>

下面包含的所有代码片段都在 boost::serialization 命名空间内定义。 shared_ptr < T > 在 shared_ptr.hpp 中定义。 shared_ptr 的一般类轮廓如下&#xff1a; shared_ptr 包括以下成员&#xff1a; T *px;shared_count pn;&#xff0c;其中包含指向&#xff1a; sp_c…

(二十七)大数据实战——hbase高可用集群安装与部署

前言 本节内容我们主要介绍HBase高可用集群的安装部署。HBase是一个开源的分布式非关系型数据库管理系统&#xff08;NoSQL&#xff09;&#xff0c;它运行在Apache Hadoop之上。它基于Google的Bigtable论文设计&#xff0c;并且具有高扩展性、高可靠性和高性能的特点。HBase通…

Python 04 之变量【列表,元组,集合,字典,字符串】

&#x1f600;前言 在Python编程语言中&#xff0c;我们经常会遇到各种数据类型和相应的操作方法。理解和掌握这些基本构造是进行有效编程的前提。在本文中&#xff0c;我们将介绍两种非常重要的数据结构 - 集合和字典&#xff0c;然后我们将深入探讨字符串及其相关的操作和处理…

父域 Cookie实现sso单点登录

单点登录&#xff08;Single Sign On, SSO&#xff09;是指在同一帐号平台下的多个应用系统中&#xff0c;用户只需登录一次&#xff0c;即可访问所有相互信任的应用系统。Cookie 的作用域由 domain 属性和 path 属性共同决定。在 Tomcat 中&#xff0c;domain 属性默认为当前域…

CUDA小白 - NPP(8) 图像处理 Morphological Operations

cuda小白 原始API链接 NPP GPU架构近些年也有不少的变化&#xff0c;具体的可以参考别的博主的介绍&#xff0c;都比较详细。还有一些cuda中的专有名词的含义&#xff0c;可以参考《详解CUDA的Context、Stream、Warp、SM、SP、Kernel、Block、Grid》 常见的NppStatus&#xf…

消除笔哪个P图软件有?这几种软件都有消除笔功能

哪些软件中有消除笔工具呢&#xff1f;我们在日常的生活中&#xff0c;会经常有编辑图片的需求&#xff0c;如果图片上有一些内容我们想要将它去除掉&#xff0c;如文字、涂鸦、笔记、标记等&#xff0c;需要用到一些消除笔工具&#xff0c;那么哪些软件具有这个功能并且还非常…

Excel变天了!国内已经可以用Python了!看看如何操作

对于大部分学python的同学来说&#xff0c;绝大部分场景都是用Pandas处理excel。 但有时简单的处理还要打开Jupyter或者VS Code&#xff0c;就有点麻烦。 现在&#xff01;微软已经把Python塞到Excel里啦&#xff01; 其实之前就已经塞了&#xff0c;但这几天国内都可以用了。…

传猪场员工因抑郁症去世,ACM金牌

前言 一位素未蒙面的学弟&#xff0c;R.I.P 既然是 “传”&#xff0c;我们就不能假定人家有抑郁症&#xff0c;其实前天就收到了这个消息&#xff0c;因为是一个学校的&#xff0c;又是ACM金牌&#xff0c;所以第一时间就在群里刷屏了&#xff0c;这件事情对于一个家庭来说&am…

10个TikTok影响力营销策略,让你的品牌崭露头角

TikTok已经成为一种崭露头角和塑造品牌声誉的强大平台。随着数以亿计的用户在这个短视频应用上分享创意和内容&#xff0c;品牌和营销专业人士也越来越多地将其作为推广产品和服务的渠道。 在本文中&#xff0c;我们将探讨10个TikTok影响力营销策略&#xff0c;帮助你的品牌在…

【Spring Boot】有这一文就够了

作者简介 前言 作者之前写过一个Spring Boot的系列&#xff0c;包含自动装配原理、MVC、安全、监控、集成数据库、集成Redis、日志、定时任务、异步任务等内容&#xff0c;本文将会一文拉通来总结这所有内容&#xff0c;不骗人&#xff0c;一文快速入门Spring Boot。 专栏地址…

了解CRM软件系统三种类型的特点与区别

市面上的CRM系统大致可以分为三种主要类型&#xff1a;分析型CRM、运营型CRM和协作型CRM。很多人对这三种类型的CRM系统不太了解&#xff0c;不知道该如何区分&#xff0c;下面我们就来说说CRM系统的3种类型&#xff1a;分析型、运营型和协作型的区别。 分析型CRM的特点&#…

系统灰度随笔记

系统灰度随笔记 这段时间系统重构&#xff0c;负责重构的其中一个模块需要与四个上游系统对接进行切换&#xff0c;虽然自己在这个过程中也设计了一套灰度方案来承接&#xff0c;将灰度的主动权控制在下游&#xff0c;但是很难同时应对四个上游系统&#xff0c;因为每个上游系…

Python语言学习实战-内置函数reduce()的使用(附源码和实现效果)

实现功能 reduce()是一个内置函数&#xff0c;它用于对一个可迭代对象中的元素进行累积操作。它接受一个函数和一个可迭代对象作为参数&#xff0c;并返回一个单个的累积结果。reduce()函数的语法如下&#xff1a; reduce(function, iterable[, initializer])其中&#xff0c;…

SpringMVC之JSON返回及异常处理

目录 JSON处理 导入依赖 配置Spring-mvc.xml ResponseBody注解使用 测试 目录 JSON处理 导入依赖 配置Spring-mvc.xml ResponseBody注解使用 测试 Jackson 定义 用法 常用注解 统一异常处理 为什么要全局异常处理&#xff1f; 异常处理思路 SpringMVC异常分类 综…

java基础-基础知识点

文章目录 jdk目录结构函数式接口wait、notify、notifyAll 并发编程Threadsleep、yield、joindaemon &#xff08;守护线程&#xff09; 锁[synchronized ](https://blog.csdn.net/EnjoyFight/article/details/127457876)线程池 jdk目录结构 jdk1.8 jdk20 函数式接口 http…

PyTorch之张量的相关操作大全 ->(个人学习记录笔记)

文章目录 Torch1. 张量的创建1.1 直接创建1.1.1 torch.tensor1.1.2 torch.from_numpy(ndarray) 1.2 依据数值创建1.2.1 torch.zeros1.2.2 torch.zeros_like1.2.3 torch.ones1.2.4 torch.ones_like1.2.5 torch.full1.2.6 torch.full_like1.2.7 torch.arange1.2.8 torch.linspace…

快速安装Redis以及配置Redis集群

Redis集群 本章是基于CentOS7下的Redis集群教程&#xff0c;包括&#xff1a; 单机安装RedisRedis主从Redis分片集群 1.单机安装Redis 首先需要安装Redis所需要的依赖&#xff1a; yum install -y gcc tcl#docker安装redis #1、docker pull redis#2、docker run --name my…

如何搭建一款BI系统

一、BI系统介绍 1.1 什么是BI系统 BI的英文全拼是Business Intelligence&#xff0c;商业智能&#xff0c;简称BI。我们经常能听到企业说“上BI”、“建设BI系统”、“构建BI决策平台”等内容。那么BI到底是什么呢&#xff1f; (1) 最初起源于固定报表 在几十年前&#xff…