【Linux】网络套接字编程

前言

        在掌握一定的网络基础,我们便可以先从代码入手,利用UDP协议/TCP协议进行编写套接字程序,明白网络中服务器端与客户端之间如何进行连接并且通信的。

目录

一、了解源目的IP、端口、网络字节序、套接字

端口号:

套接字:

认识传输层上的TCP/UDP协议:

网络字节序:

常见的套接字:

二、UDP网络编程

1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端。

socket

bind

本地字节序和网络字节序相互转化

recvfrom&recv&recvmsg

send&sendto&sendmsg

客户端:

127.0.0.1本地回环地址

 2.0 windows客户端向Linux服务器发送消息,服务器将此消息返回给对应的客户端。

三、TCP网络编程

1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端。

listen

accept

connect


一、了解源目的IP、端口、网络字节序、套接字

        首先,我们知道两个主机进行网络通信,那么就必须需要一个源IP和一个目的IP。根据此IP(这里IP就认为是公网IP,在特定的区域保证了其唯一性),能够确定在全网的一个唯一性。

        在如下主机之间进行通信,我们理解一下通信的目的是什么:

         通常情况下,把数据送到对方的机器是目的吗?可以发现,机器只是充当工具,是机器上的软件在进行通信。所以真正的网络通信过程本质就是进程间通信!将数据在主机间转发仅仅是手段。机器收到后,交付给指定的进程!

        因为牵扯到了进程,当其中一个主机接收到网络信息之后,通过解包如何能够执行对应存在内存中的网络进程呢?这就和端口号相关了。

端口号:

        传输层协议的内容。是标识特定主机上的网络进程的唯一性!

        为何是网络进程的唯一性呢?首先是主机IP在全网的唯一性+进程在此主机里面的唯一性。所以此组合在全网就是唯一的进程。

        并且端口号和进程id做了区分,解耦-进程id就管理系统的那一套,端口号就管网络的这一套。对于进程来说,一个进程可以绑定多个端口号(即存在不同的IP对其进程进行通信),但是端口号只能对于一个进程。(否则就确定不了唯一性了)

        当然,既然IP存在源IP和目标IP,对于端口号来说也存在源端口号和目的端口号,表示是谁发的,谁来接收。

套接字:

        实际上,我们将源IP和源端口号组成一起,目标IP和目标端口号组合在一起,这就是套接字

        Socket = {IP: 端口号};

        端口号是16位。

认识传输层上的TCP/UDP协议:

UDP:用户数据报协议
连接:无连接
可靠:不可靠传输(存在丢包等问题)

面向数据报

TCP:传输控制协议
连接:有连接
可靠:可靠传输
面向字节流

        可靠指的是中性描述。出现丢包问题,在有些场景下容忍不可靠的。可靠性是需要大量的编码和处理的。UDP只是把数据发出了,更加简单。---选择协议的时候,根据场景需求:比如直播、视频网站适合使用UDP协议去做。

网络字节序:

        我们知道,在计算机内存中保存数据时,根据高位和低位与地址的高位低位存在区别-即大小端字节序。由于在网络通信的过程中,我们们并不知道通信的两台主机究竟是否一样的字节序,如果存储字节序相反,那么另一台主机读取数据的时候就会出错。

        所以,网络规定:所有从本地传输到网络上,必须是大端字节序。这样的话无论是以小端存储的机器还是大端存储的机器,每次从网络获取到的数据就一定是大端字节序,这样就可以方便进行转化读取就不会出现问题了。

常见的套接字:

1.域间socket  命名管道-类似
2.原始socket  编写很多工具 - 饶过很多上层协议使用底层。

3.网络socket  

        理论上是三种应用场景,对应的是三套接口。但是Linux不想设计过多的接口所以将所有的接口统一。

        并且为了管理套接字内的内容,定义了sockaaddr结构体,一个通用(所有关于套接字接口统一的类型),另外是分别针对不同套接字不同的结构体,方便相互转化,统一接口的使用。

 sockaddr结构:
    网络套接字:标志类型 16位端口 32IP地址    _in    AF_INET   // PF_INET
    域间套接字:标志类型 108byte路径名         _un    AF_UNIX
    通用:前两个字节:标志类型         sockaddr
    ....

二、UDP网络编程

         初识了上面的预备内容,接下了通过写代码的过程,将UDP协议网络编程的接口介绍,并且能够真正的做出客户端和服务器端进行简单聊天通信的过程:

1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端

服务器端

        我们对Server服务器端进行一个封装,封装为.hpp文件,我设想能够对其服务器进行初始化,启动两个步骤。

UDPServer.hpp

         首先确定成员属性。对于socket编程来说,首先不可缺少的属性就是socket即套接字,实际上就是一个类似文件描述符的东西(fd),类型为int即可。其次就是ip和端口了。注意ip是16位整数,ip为32位。但是ip通常用点分十进制进行表示,用.分隔的每部分数字均为1字节,无符号的话那么表示就是0~255。

// 服务器封装
class UDPServer
{
    //......
private:
    // 源套接字 = 源ip + 源端口
    std::string _SRCip;
    uint16_t _SRCport;
    // UDP对应一套读写套接字对应文件描述符
    int _socket;
};

        然后就是构造函数,构造函数需要对这些属性进行初始化。下述代码为什么设置ip默认参数为空,下面运行代码里会说明。

class UDPServer
{
public:
    UDPServer(uint16_t port, std::string ip = "")
        : _SRCip(ip), _SRCport(port), _socket(-1)
    {
    }
//......
};

         然后就是初始化。对于UDP协议服务器来说,初始化首先我们需要将我们创建的套接字与本地传入的ip以及端口号进行绑定。此时涉及到网络接口调用,这里我们一个一个介绍:

        首先创建套接字:

socket

头文件

        #include <sys/types.h> 

        #include <sys/socket.h>

函数原型

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

函数介绍

        socket() creates an endpoint for communication and returns a descriptor.(Socket()为通信创建一个端点并返回一个描述符。)

        domain:此参数指定通信域;这将选择用于通信的协议族。(IPV4 - AF_INET(IPV6就是后面加个6))

        type:指定的类型,该类型指定通信语义。UDP为SOCK_DGRAM - 数据报 TCP为SOCK_STREAM流式。

        protocol:协议指定套接字要使用的特定协议。实际通过前两个参数选择就能自动推导出了,设置为0即可。

        返回值:如果成功,则返回新套接字的文件描述符。如果出现错误,则返回-1,并适当地设置errno。

        创建完套接字后,我们需要对本地IP和端口进行绑定,使用接口bind:

bind

头文件

        #include <sys/socket.h>

函数原型

        int bind(int socket, const struct sockaddr *address, socklen_t address_len);

函数介绍

        socket:套接字文件描述符。

        address:套接字结构体:

               对于网络来说,需要使用结构struct sockaddr_in,之后传参的时候进行强转struct sockaddr即可。

                struct sockaddr_in包含的内容属性如下:

                sin_family - 协议家族  sin_port - 端口  sin_addr.s_addr - ip

                对于ip,服务器一般设置为0.0.0.0 即0,这表示此服务器可以接收任何ip(因为一个服务器可能存在多个网卡的,让服务器在工作过程中可以从任意IP中获取数据)也推荐如此绑定,设置宏即可INADDR_ANY。

                需要注意的是协议家族和上面的指定通信域一致,另外port和s_addr因为要发送到网络里去,所以需要将本地字节序转化为网络字节序。另外在初始化前,需要对其进行清零操作,可以使用函数memset或者bzero清零。

        address_len:套接字结构体的大小。

        返回值:成功完成后,bind()将返回0;否则,将返回-1,并设置errno表示错误。

        因为在初始化套接字结构体的时候,要对属性转为网络字节序,有如下接口提供选择:

本地字节序和网络字节序相互转化

整型转化

        #include <arpa/inet.h>

       uint32_t htonl(uint32_t hostlong);//将无符号整数(32字节)hostlong从主机字节序转换为网络字节序。

       uint16_t htons(uint16_t hostshort);//将无符号短整型(16字节)hostshort从主机字节序转换为网络字节序。

       uint32_t ntohl(uint32_t netlong);//将无符号整数(32字节)netlong从网络字节序转换为主机字节序。

       uint16_t ntohs(uint16_t netshort);//将无符号短整型(16字节)netshort从网络字节序转换为主机字节序。

上述接口一般用于端口转化中,对于ip由于常用是字符串进行表示,也有相应接口进行转化为数字以及网络字节序。

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

       int inet_aton(const char *cp, struct in_addr *inp);

//将互联网主机地址cp从IPv4的数字点表示法转换为二进制形式(按网络字节序),并将其存储在inp指向的结构中。inet_aton ()如果地址有效则返回非零,否则返回零。

       in_addr_t inet_addr(const char *cp);

//将互联网主机地址cp从IPv4的点号表示法转换为网络字节序的二进制数据。如果输入无效,INADDR_NONE(通常是-1)就是返回。使用这个函数会有问题,因为-1是有效的地址(255.255.255.255)。应避免使用它,而应使用inet_aton()、inet_pton(3)或getaddrinfo(3),它们提供了一种更简洁的方式表示错误返回。

       in_addr_t inet_network(const char *cp);

// 将cp (IPv4数字点表示法的字符串)转换为主机字节序的数字,以便用作Internet的网络地址。若成功,则依此返回地址。如果输入无效,则返回-1。

       char *inet_ntoa(struct in_addr in);

//将以网络字节顺序给出的互联网主机地址转换为IPv4点分十进制记数法表示的字符串。字符串被返回到一个静态分配的缓冲区中,那些后续调用将被覆盖。

       struct in_addr inet_makeaddr(int net, int host);

//是inet_netof()和inet_lnaof()的逆函数。它以网络字节顺序返回Internet主机地址,由网络编号net和创建本地地址主机,均按主机字节顺序。

       in_addr_t inet_lnaof(struct in_addr in);

//返回的是Internet地址中的本地网络地址。返回值按主机字节顺序排列。

       in_addr_t inet_netof(struct in_addr in);

//函数的作用是:返回Internet地址中的网络号部分。返回值按主机字节顺序排列。

        如此,利用上述接口,我们就可以将套接字与服务器ip和地址进行一个绑定。

class UDPServer
{
public:
//......
// UDP服务器初始化:创建套接字+绑定
    void initserver()
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0); // 网络-IPV4  面向数据报 协议-填0即可,会根据前面两个选项进行判断
        if (_socket < 0)
        {
            // 返回-1表示创建套接字失败,致命错误
            logMessage(FATAL, "套接字创建失败-%d:%s", errno, strerror(errno));
            exit(1);
        }

        // 绑定本地进程
        struct sockaddr_in local; // 注意头文件必须包含完 man in_addr
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;                                                      // 协议家族
        local.sin_port = htons(_SRCport);                                                // 注意,此处是要发送到网上的,需要转化为网络字节序,使用接口 hton 本地转网络 s是2字节16位
        local.sin_addr.s_addr = _SRCip.empty() ? INADDR_ANY : inet_addr(_SRCip.c_str()); // 如果为空,设置默认ip,此时可以接收任意ip发送的消息,不局限于一个ip。
        // 上述套接字结构初始化完毕,现在进行绑定
        if (bind(_socket, (struct sockaddr *)&local, sizeof local) < 0)
        {
            // 小于零绑定失败!
            logMessage(FATAL, "绑定失败-%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "UDP服务器初始化成功..... %s", strerror(errno));
        // UDP无连接 -初始化完成-
    }
//......
};

        注意上述日志(logMessage)利用的是以前我博客中的log.h文件编写,可以利用print或者cout代替。

        初始化完成后,我们需要编写服务器启动函数。由于UDP协议无需要连接,利用数据报传输,所以利用如下接口进行发送和接收目标主机信息即可:

recvfrom&recv&recvmsg

头文件

       #include <sys/types.h>
       #include <sys/socket.h>

函数原型

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

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

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

函数介绍

        recvfrom()和recvmsg()用于从套接字中接收消息,无论套接字是否面向连接,都可以使用它们接收数据。通常用于UDP协议套接字编程。

        recv()调用通常只在已连接的套接字上使用(参见connect(2)),它与recvfrom()带有NULL src_addr参数相同。通常用于TCP协议套接字编程。

        sockfd为对应套接字文件描述符,buf均为接收信息所存储的缓冲区,len为缓冲区的大小,flags为读取的方式,一般设置为0为阻塞读取。src_addr为传输信息的主机的套接字结构,addrlen是输入输出参数,输入需要传入原本套接字结构体的大小,返回就是返回后的套接字结构大小。

        返回值:这些调用返回接收到的字节数,如果发生错误则返回-1。如果发生错误,则设置errno来指示错误。当对端执行完毕时,返回值为0有序关闭。

        特别的,对于TCP协议中(recv、read),如果返回值>0就是正常读取,当返回值等于0的时候,说明对端已经关闭了连接了,当返回值小于0的时候,说明读取失败,设置错误码。

send&sendto&sendmsg

头文件

       #include <sys/types.h>
       #include <sys/socket.h>

函数原型

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

函数介绍

        send()调用只能在套接字处于连接状态时使用(以便知道预期的接收者)。TCP协议套接字编程使用。

        sendto一般用于UDP协议套接字编程。

        其中sockfd为套接字文件描述符,buf为要发送的存储区,len为此区大小,flags通常设为0,dest_addr为要发送的给目标主机的套接字结构体,addrlen就是此结构体的大小。

        返回值:成功时,这些调用返回发送的字符数。如果出现错误,则返回-1,并适当地设置errno。

        服务器启动函数,首先明确服务器是一个常驻进程,那么必然是死循环,并且不断接收不同客户端对其发送的消息。这里要求时将发送的消息打回去。可以利用接收到的目标主机的port和ip对其内容在服务器端显示,然后将原数据进行返回即可。

class UDPServer
{
public:
//......
    // UDP服务器通信开始!
    void start()
    {
        // 正式启动UDP服务器
        // 1.0版本 UDP接收客户端信息,返回给客户端本身消息
        char buffer[1024];
        while (true) // 常驻进程,永远不退出
        {
            // 创建客户端套接字结构,用来接收
            struct sockaddr_in client;
            socklen_t clientlen = sizeof(client);
            bzero(&client, sizeof client); // 清零空间
            // 面向数据报接收消息
            ssize_t n = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &clientlen); // 0阻塞读取
            if (n > 0)
            {
                buffer[n] = '\0';
                // 可以把对应主机的套接字信息提取出来
                std::string clientip = inet_ntoa(client.sin_addr); // 网络 转化ip
                uint16_t clientport = ntohs(client.sin_port);
                printf("[%s: %d]# %s\n", clientip.c_str(), clientport, buffer);
            }
            // 发送对应主机
            sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&client, sizeof(client));
        }
    }
//......
};

        析构函数释放掉套接字内存即可,需要注意判断是否初始化,可以利用默认给的套接字的值进行判断。

class UDPServer
{
public:
//.......
    ~UDPServer()
    {
        if (_socket != -1)
            close(_socket);
    }
//.......
};

UDPServer.cpp

        然后,我们在源文件中,利用命令行参数,对服务器进行挂接启动即可。

#include "UDPServer.hpp"
#include <iostream>
#include <memory>

static void UserManual()
{
    std::cout << "please:./UDPServer port" << std::endl;
}

int main(int arc, char* argv[])
{
    if (arc != 2)
    {
        UserManual();
        exit(-1);
    }
    std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(atoi(argv[1])));
    UDPServer_ptr->initserver();
    UDPServer_ptr->start();
    return 0;
}

客户端:

        客户端使用接口上面已经介绍完毕,下面不在进行重复介绍。

        客户端,首先自然需要获取服务器的ip和对应端口,然后给服务器端创建套接字结构使用接口sento发送过去即可。

        那么我们这里需要想一下问题:客户端的套接字是否要绑定本地ip和端口呢?一般对于客户端,实际上就是应用。如果应用每次绑定特定的端口的话,根据之前所了解的网络基础我们可以知道,端口对于唯一的一个进程,那么不同的应用编写的时候很有可能存在绑定一样的端口,那么此时使用就会存在问题,即一个端口就对应了不同的进程了,服务器端返回数据不知道发送给谁。

        所以,客户端套接字不需要自己绑定ip和端口,在发送给服务器首次的时候,便由操作系统随机安排,那么这样就可以避免端口号冲突的问题。

        客户端不再进行封装,简易代码如下:

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

static void UserManual()
{
    std::cout << "please:./UDPClient ip port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        UserManual();
        exit(-1);
    }
    // 首先创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    // 注意,此套接字不可显示绑定 - 不能绑定确定的port

    // 将服务器端套接字结构初始化好
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);  // 首先点分式转化为数字然后转化为网络字节序保存起来

    char buffer[1024];
    while (true)
    {
        std::cout << "请输入# ";
        std::string message;
        std::getline(std::cin, message);
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);

        // 接收消息
        struct sockaddr_in _server;
        socklen_t server_len = sizeof(_server);
        bzero(&_server, server_len);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&_server, &server_len);
        if (s > 0)
        {
            buffer[s] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}

        我们在Linux下运行代码尝试:

127.0.0.1本地回环地址

        首先进行本地测试,本地测试意思就是给服务器绑定ip 127.0.0.1,它是本地回环地址(loopback address),即不会上传到网络,而是在本机内测试,同样的本地协议栈还是会跑一遍,但是不会经过网络接口。

        这样客户端和服务器首先本地跑一遍排查问题后就可以接入网去跑了,但是注意需要简单修改一下上面服务器写的命令行参数的处理。

// 在文件UDPServer.cpp 服务器源文件内修改
int main(int arc, char* argv[])
{
    int port;
    std::string ip = "";
    if (arc == 2)
    {
        port = atoi(argv[1]);
    }
    else if (arc == 3)
    {
        port = atoi(argv[2]);
        ip = argv[1];
    }
    else
    {
        UserManual();
        exit(-1);
    }

    std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(port, ip));
    UDPServer_ptr->initserver();
    UDPServer_ptr->start();
    return 0;
}

        测试效果如下:

        本地测试成功,那么转入网络在来试一次:

 (打马赛克的地方是自己的服务器公网ip)

        在Linux环境下测试后,我们可不可以与windows系统上的进行通信呢?

 2.0 windows客户端向Linux服务器发送消息,服务器将此消息返回给对应的客户端

        实际上,在不同的操作系统环境下,虽然有些系统调用变量,但是对于socket套接字编程来说对于网络就是一套的。对于UDP协议来说,windows客户端相对于Linux客户端(或者说windows下的网络编程)多了如下的初始化:

#include <WinSock2.h>  // 引入套接字编程库 windows为此库 只需引入一个库就可以了

#pragma warning(disable:4996)  // 屏蔽错误  一般用一些不安全的函数,可以利用此进行屏蔽
#pragma comment(lib, "ws2_32.lib")  // 固定用法,加载入库

int main()
{
	WSADATA WSAData;  // win 初始化
	if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
	{
		//WSACleanup();
		std::cout << "初始化失败!" << std::endl;
		return -1;
	}

    // ......

	closesocket(sock);
	WSACleanup();  // 结束
    return 0;
}

        其余基本不变。

         可以看到,windows端依然是可以向Linux端的服务器发送信息的。但是使用UDP协议进行网络通信的话,发送中文会存在乱码问题,需要转码解决。

        根据上述的两个UDP代码操作实例,我们可以发现问题:

1.首先UDP协议的套接字文件描述符是既可以写也可以读,说明UDP协议的套接字文件描述符是全双工的。

2.UDP协议并没有任何连接,只要创建和初始化套接字相关信息后就直接进行了通信。

        我们看看接下来的TCP协议和UDP协议编写的网络有什么不同,并且对于TCP服务器端使用一些功能让我们的网络服务更加复杂和形象化。

三、TCP网络编程

1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端

        首先,还是从最基础的开始。

TCPServer.hpp

        首先还是将TCP服务器进行封装,完成以下的两个功能:1.初始化 2.启动。

        针对于UDP服务器的编码,TCP在编码实现上在创建服务器套接字,绑定服务器ip和端口后,UDP是初始化完了的,但是注意TCP协议与UDP协议一个最重要的区别就是有无连接。TCP是有连接的,所以一般就需要区分套接字类型。

        好比一家饭店,店主专门派一个成员在马路边喊客人,当此成员(后面称为A成员)将客人喊道后,就交给店内的服务员去处理,然后A成员就继续去喊客去了。TCP的套接字实现实际上就是这样,分为两个套接字,一个监听套接字,一个处理或者可以称为服务套接字。监听套接字利用接口返回服务套接字的文件描述符,里面就已经绑定了客户端的套接字相关信息,并且因为是支持字节流的,所以也能通过read和write等文件操作函数进行操作。

        那么在初始化的时候,最后需要设置监听套接字为监听模式,此时方为初始化完毕。

listen

头文件

       #include <sys/types.h>
       #include <sys/socket.h>

函数原型

        int listen(int sockfd, int backlog);

函数介绍

        listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2)(listen() 将 sockfd 引用的套接字标记为被动套接字,即作为将用于使用 accept(2) 接受传入连接请求的套接字)

        sockfd:套接字文件描述符。

        backlog:backlog 参数定义了 sockfd 的挂起连接队列可能增长到的最大长度。 如果连接请求在队列已满时到达,客户端可能会收到一个 带有 ECONNREFUSED 指示的错误,或者,如果底层协议支持重传,则可以忽略该请求,以便稍后重新尝试连接成功。

        返回值:成功时,返回零。 出错时返回 -1,并适当设置 errno。

        启动的话,对于UDP协议来说,是直接通信的,因为接收比如recvform存在客户端套接字结构体的相关信息。但是对于TCP来说,此时首先需要监听套接字在监听模式下,与当前服务器端进行连接的客户端进行连接,返回一个服务套接字,此时对于此服务套接字就可以对客户端的信息进行提取或者发送了。也就是说此时就连接上了一个客户端,而监听套接字返回之后就继续去监听去了,直到监听到又会继续返回。

accept

头文件

       #include <sys/types.h>
       #include <sys/socket.h>

函数原型

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

函数介绍

        accept() 系统调用与基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)一起使用。 它提取挂起连接队列中的第一个连接请求以进行监听,套接字sockfd创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符。 新创建的套接字没有处于监听状态。 原来的socket不受此调用的影响。

        sockfd:监听套接字

        addr:输出型参数,接收客户端的套接字结构体。

        addrlen:输入输出型参数,首先输入addr的大小,返回是返回接收客户端的套接字结构体大小。

        返回值:正确返回一个非负整数,如果错误返回-1并且设置errno。

        通过上面接口的介绍,我们就可以基本进行封装一下,因为1.0版本是只是单纯的服务器端返回给客户端数据,我们先实现下面的单进程或者线程版本:

#ifndef _TCPSERVER_
#define _TCPSERVER_

#include "log.hpp"
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

const static int gbacklog = 20; // 一般不能设置太大和太小 -后面再说

static void handleTheClient(int &serviceSock, const std::string &clientIp, uint16_t clientPort)
{
    char buffer[1024];
    while (true)
    {
        printf("[%s: %d]# ", clientIp.c_str(), clientPort);
        // 接收的话注意是字节流,所以以前的文件那一套可以使用的
        ssize_t n = recv(serviceSock, buffer, sizeof(buffer) - 1, 0); // 阻塞等待,没有from就是适合面向字节流的,即TCP的,但是UDP写可以用的
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << buffer << std::endl;

            // 然后发送
            // 可以使用以前文件操作那一套或者send
            // send(serviceSock, buffer, sizeof(buffer) - 1, 0);  // 阻塞发送,使用处理套接字
            ssize_t s = write(serviceSock, buffer, strlen(buffer));
            if (s < 0)
            {
                logMessage(ERROR, "发送信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                break;
            }
        }
        else if (n == 0)
        {
            // 对方关闭了连接,这里也进行关闭
            printf("[%s: %d]客户端关闭连接,我也关闭此连接\n");
            close(serviceSock);
            serviceSock = -1; // 防止后面析构再次释放
            break;
        }
        else
        {
            // 小于零说明接收失败
            logMessage(ERROR, "接收信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            close(serviceSock);
            break;
        }
    }
}

class TCPServer
{
public:
    TCPServer(uint16_t port, std::string ip = "") : _server_ip(ip), _server_port(port),
                                                    _listen_sock(-1), _service_sock(-1)
    {
    }

    void initTCPServer()
    {
        // 初始化TCP服务器
        _listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 流式套接 自动识别为TCP协议,面向字节流
        if (_listen_sock < 0)
        {
            logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(1);
        }

        // 服务器将自己的ip和端口绑定到对应的套接字上。
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof server_addr); // 初始化为0
        server_addr.sin_family = AF_INET;            // 家族还是网络IPV4
        server_addr.sin_port = htons(_server_port);  // 转化为网络字节序的端口号
        if (_server_ip.empty())
            server_addr.sin_addr.s_addr = INADDR_ANY;  // 如果是空字符串就如此处理
        else
        {
            int n = inet_aton(_server_ip.c_str(), &server_addr.sin_addr); // 直接写入结构中,将点分式的ip地址转化为数字然后转化为网络字节序
            if (n == 0)
            {
                logMessage(FATAL, "写入ip地址无效!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                exit(2);
            }
        }

        // bind
        if (bind(_listen_sock, (struct sockaddr *)&server_addr, sizeof server_addr) < 0)
        {
            logMessage(FATAL, "服务器端ip与port与套接字绑定失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(3);
        }
        logMessage(NORMAL, "绑定成功!......");
        // UDP到这一步初始化完毕,但是TCP还存在一步,需要进行连接
        // 因为TCP是面向连接的,我们正式通信前需要先建立连接
        // 此时就相当于设置_sock套接字为监听模式了
        if (listen(_listen_sock, gbacklog) < 0)
        {
            logMessage(FATAL, "连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(4);
        }
    }

    // 初始化完就是启动了
    void start()
    {
        // 服务器先接收消息,然后在发送消息
        // TCP协议能否简单的像UDP那样直接进行通信吗?显然不能,在连接阶段使用的套接字是监听套接字,对信息处理并且发送是处理套接字所要干的事情
        struct sockaddr_in clientAddr;
        socklen_t clientAddrLen = sizeof clientAddr; // 用来接收客户端信息的套接字结构体
        while (true)
        {
            // 首先确保常驻
            // 首先获取连接,连接我返回,不连接在这里进行阻塞
            _service_sock = accept(_listen_sock, (struct sockaddr *)&clientAddr, &clientAddrLen);
            if (_service_sock == -1)
            {
                logMessage(ERROR, "连接客户端失败,重新连接... %d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                continue;
                // 连接失败不是致命的错误,重连一次即可
            }
            logMessage(NORMAL, "客户端连接成功.....");
            // 现在使用堆客户端信息处理的代码即可,这里我将他们封装为一个函数
            // 首先在外面先获取客户端的ip和端口
            std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 将网络字节序的网络地址ip转化为点分十进制的字符串
            uint16_t clientPort = ntohs(clientAddr.sin_port);      // 网络转为本地字节序,注意是16位整数

            // 处理信息
            handleTheClient(_service_sock, clientIp, clientPort);
        }
    }

    ~TCPServer()
    {
        if (_listen_sock != -1)
            close(_listen_sock);
        if (_service_sock != -1)
            close(_service_sock);
    }

private:
    std::string _server_ip;
    uint16_t _server_port;
    int _listen_sock;  // 监听套接字
    int _service_sock; // 处理套接字
};

#endif

TCPServer.cpp

        然后在此文件内调用创建即可。

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

static void UserManual()
{
    std::cout << "please:./TCPServer ip port or /TCPServer port" << std::endl;
}

int main(int argc, char* argv[])
{
    std::string ip = "";
    uint16_t port;
    if (argc == 2)
    {
        port = atoi(argv[1]);
    }
    else if (argc == 3)
    {
        ip = argv[1];
        port = atoi(argv[2]);
    }
    else
    {
        UserManual();
        exit(-1);
    }

    std::unique_ptr<TCPServer> TCP_server(new TCPServer(port, ip));
    TCP_server->initTCPServer();
    TCP_server->start();
    return 0;
}

TCPClient.cpp 

        对于客户端来说,首先我们不对其进行封装。

        在UDP中我们已经介绍过,由于客户端的特殊性,所以让操作系统默认的帮我们绑定ip和端口,所以客户端只需要创建好套接字,以及初始化好服务器端的套接字结构体即可。上述步骤依旧一致,但是从此时开始UDP就直接可以接收了-数据报、无连接。

        TCP是基于连接、字节流的,所以自然客户端首先需要和服务器端建立连接。(三次握手) 在建立连接后,此时客户端的套接字就可以正常的与服务器端进行通信了。此时可以将客户端的套接字视为一个普通的文件描述符,并且是可读可写,所以文件操作就可以一起用上了。

        当然,客户端最后和服务器端需要断开连接。(四次挥手)

        下面我们简单介绍一下客户端连接服务器端的接口函数:connect

connect

头文件

       #include <sys/types.h>
       #include <sys/socket.h>

函数原型

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

函数介绍

        connect() 系统调用将文件描述符 sockfd 引用的套接字连接到 addr 指定的地址。

        sockfd:客户端套接字

        addr:服务器端套接字结构体

        addrlen:服务器端套接字结构体大小

        返回值:连接成功返回0,否则返回-1。

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

static void UserManual()
{
    std::cout << "please:./TCPclient server_ip server_port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        UserManual();
        exit(-1);
    }

    // 创建客户端的套接字
    int clientSock = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSock < 0)
    {
        logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(1);
    }

    // TCP客户端同样的不需要主动绑定自己的ip和port
    // 对于TCP客户端来说,需要的是连接能力,那么一个套接字足以
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof server_addr);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));  // 是要发送到网络上的,所以千万别忘了转为网络字节序
    if (0 > inet_aton(argv[1], &server_addr.sin_addr))  // 主机转网络 ip 点分十进制
    {
        logMessage(FATAL, "ip从本地转化网络字节序失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(2);
    }
    socklen_t sever_len = sizeof server_addr;
    if ( 0 > connect(clientSock, (struct sockaddr*)&server_addr, sever_len))
    {
        logMessage(FATAL, "服务器连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(3);
    }
    logMessage(NORMAL, "服务器连接成功~");

    // 客户端不断向服务器端发送信息接收信息即可
    std::string message;
    char buffer[1024];
    while (true)
    {
        printf("请输入# ");
        std::getline(std::cin, message);
        if (message == "quit") break;

        // 使用send可以发送
        if ( 0 > send(clientSock, message.c_str(), message.size(), 0))  // 阻塞发送
        {
            logMessage(ERROR, "客户端发送失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            continue;
        }

        // 使用read接收
        ssize_t n = read(clientSock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "server# " << buffer << std::endl;
        }
        else if (n == 0)
        {
            // 此时对端关闭,我也关闭即可
            break;
        }
        else
        {
            logMessage(FATAL, "接收服务器数据失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(4);
        }
    }
    close(clientSock);
    return 0;
}

        上述就是TCP实现套接字编程的大致过程与思路。基本接口均已介绍完毕,这里不再演示效果。如果有后续补充我会继续在此时写,如果有错误还请大佬指出!

        另外,对于TCP来说,我们在Linux环境下可以使用netstat -antp查看tcp协议的服务,l是只查看监听状态  t就是tcp,换为u就是udp了。

        存在一个工具可以简单替代一下TCP的客户端:telnet 如果没有请使用sydo yum install telnet 进行安装即可。enter 运行,ctrl+] + quit可以退出。

猜你喜欢

转载自blog.csdn.net/weixin_61508423/article/details/129086875