【网络】socket和udp协议


一、六个背景知识

1、Q1:在进行网络通信时,是不是两台机器在进行通信?

答:不是的,是应用层在通信。
解析:网络协议中的下三层,主要解决的是数据安全可靠的送到远端机器。用户使用应用层软件完成数据发送和接收的。
而在使用软件的时候,必须得先启动软件,例如我们想刷抖音就需要先把抖音这个软件启动,运行以后就是进程。所以我们网络在进行通信和收发消息的时候,就是进程间通信啊!只不过是进程之间遵守了网络协议栈,用的是网络协议的系统调用接口罢了,其本质还是进程间通信。手段是两台主机通信,而目的和本质是进程之间的通信,是凌驾于应用层上的进程之间的通信。通过网络协议栈读取网络资源(共享内存资源)来让两台主机读取/存放信息。可以用读者写者这个问题来进行理解,我们的网络资源就是缓冲区的概念,读者写者在缓冲区(网络资源)中读取/存放资源。

2、端口号

在这里插入图片描述

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

注意概念:
在公网上,ip地址能标识唯一的一台主机,端口号port能标识该主机上的唯一一个进程,故:ip:port能标识全网唯一的一个进程。

socket:客户端和服务器在进行通信的时候,客户端进程有唯一的ip地址+端口号,服务端进程也有唯一的ip地址+端口号,两者只需要进行源和目的的唯一标识即发生通信,这就是socket的概念基石。

3、端口号vs进程PID

pid已经是能够标识一台主机上的进程的唯一性的,为什么还要搞个端口号这么复杂的概念?
引入端口号是为了实现系统跟网络功能解耦合,不会引起牵一发而动全身,例如我们的学号和身份证件号的关系。

4、目的端口怎么跟客户端绑定的呢?也就是怎么通过目的端口去找到对应的进程的呢?

在这里插入图片描述

5、我们的客户端,怎么知道服务器的端口号的呢?

每一个服务器的端口号必须是众所周知的精心设计的,要被客户端熟知的,我抖音自己的开发商在开发的时候,客户端和服务端的端口号都是内置的,都是被自己熟知的,所以我们使用者感觉不到这个,但是作为开发人员是要熟知服务器的端口号的。

6、一个进程可以绑定多个端口号吗?一个端口号可以被多个进程绑定吗?

显而易见:一个进程可以绑定多个端口号,但一个端口号不能被多个进程绑定。

二、两个协议

1、TCP协议(传输控制协议)

传输层协议
有连接
可靠传输(前提网络要联通,复杂,维护性要更强)
面向字节流

2、UDP协议(用户数据报协议)

传输层协议
无连接
不可靠传输(简单,但丢包问题解决不了)
面向数据报

三、网络字节序列

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

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

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

在这里插入图片描述

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

四、socket编程接口

1、socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,
 socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
 socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
 socklen_t addrlen);

2、sockaddr结构

在这里插入图片描述

问题:为什么struct sockaddr* 不能写成void*呢?
因为刚开始网络出现的时候c语言还没有void*这个用法。

五、udp_socket_server代码编写

1、socket套接字的介绍

在这里插入图片描述

udp用的是SOCK_DGRAM,那么就是无连接不可靠的协议。

2、Log.hpp(为了打印看结果)

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // void logmessage(int level, const char *format, ...)
    // {
    //     time_t t = time(nullptr);
    //     struct tm *ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    //              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    //     // va_list s;
    //     // va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    //     // va_end(s);

    //     // 格式:默认部分+自定义部分
    //     char logtxt[SIZE * 2];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 暂时打印
    //     printLog(level, logtxt);
    // }
    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }

//     va_end(s); //s = NULL
//     return sum;
// }

3、文件描述符socket是3(套接字创建)

在这里插入图片描述
在这里插入图片描述

4、bind socket

在这里插入图片描述

// 2.绑定端口号bind socket
        struct sockaddr_in local; // 网络套接字结构体
        bzero(&local, sizeof(local)); // 将该套接字结构体对象全部清零
        local.sin_family = AF_INET; // 类型:ipv4
        local.sin_port = htons(_port); // 端口号:是在网络中来回发送的,我发过去要让对面知道我发的端口号是什么,所以必须是网络字节序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string->unit_32 2.来回通信对方要知道发送的ip,所以ip的unit_32必须是网络序列的
        int n = bind(_socketfd, (const struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            log(Fatal, "bind error, erron:%d, errno string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "bind sucess");

在这里插入图片描述

5、recvfrom和sendto

在这里插入图片描述

struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if (n < 0)
            {
                log(Warning, "recvfrom error");
                continue;
            }
            // 简单的数据处理一下
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = "server_echo#" + info;

            sendto(_socketfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr*)&client, len);

6、测试一下

在这里插入图片描述

7、两个问题(port和ip)

Q1.公网ip问题:

ip地址全0,port随意?还是我们ip是云服务器,port随意呢?
在这里插入图片描述
其实就是我们云服务器可能有好几个ip地址能够进行访问,我们单独写一个ip地址肯定是不行的,我们需要一个动态的ip地址,也就是这一个ip地址就能是整个云服务器主机所有暴露出来的ip地址,那么就简单了,我们的ip只要是全0,port随意即可。
在这里插入图片描述
Q2:系统端口号0-1023:
在这里插入图片描述

注意:0到1023是系统内定的系统端口号,一般都要有固定的应用层协议使用,例如:http对应固定端口号是80,https对应固定的端口号是443等等,所以0到1023不让普通用户去绑定,因为与本身的应用层协议是强相关的!这是因为这个应用层协议用这个端口用多了,形成了一种固定的绑定了。所以我们进行端口绑定的时候用的是1024及往上的端口号使用,我们一般用8000到9000左右,好记好用。

8、argc 和 argv的联合使用

在这里插入图片描述

在这里插入图片描述

9、总体代码

udp.hpp:

#pragma once 

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

extern Log log;

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";

enum 
{
    SOCKET_ERR=1,
    BIND_ERR
};

class UdpServer
{
public:
    // 构造函数
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : _socketfd(0)
        , _port(port)
        , _ip(ip)
        , _isrunning(false)
    {}
    void Init()
    {
        // 1.创建udp套接字socket
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        // 创建失败
        if (_socketfd < 0)
        {
            log(Fatal, "socket create error,socketfd:%d", _socketfd);
            exit(SOCKET_ERR);
        }
        // 创建成功
        log(Info, "socket create sucess,socketfd:%d", _socketfd);

        // 2.绑定端口号bind socket
        struct sockaddr_in local; // 网络套接字结构体
        bzero(&local, sizeof(local)); // 将该套接字结构体对象全部清零
        local.sin_family = AF_INET; // 类型:ipv4
        local.sin_port = htons(_port); // 端口号:是在网络中来回发送的,我发过去要让对面知道我发的端口号是什么,所以必须是网络字节序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string->unit_32 2.来回通信对方要知道发送的ip,所以ip的unit_32必须是网络序列的
        int n = bind(_socketfd, (const struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            log(Fatal, "bind error, erron:%d, errno string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "bind sucess");
    }
    void Run()
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if (n < 0)
            {
                log(Warning, "recvfrom error");
                continue;
            }
            // 简单的数据处理一下
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = "server_echo#" + info;

            sendto(_socketfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr*)&client, len);
        }
    }
    // 析构函数
    ~UdpServer()
    {
        if (_socketfd > 0) 
        {
            close(_socketfd);
        }
    }
private:
    int _socketfd; // 网络文件描述符,表示socket返回的文件描述符
    uint16_t _port; // 表明服务器进程的端口号
    std::string _ip; // ip地址,任意地址绑定为0
    bool _isrunning; // 判断是否运行
};

main.cc:

#include "udp.hpp"
#include "Log.hpp"
#include <memory>

Log log;

void Usage(std::string proc)
{
    std::cout << "\n\rUsages: " << proc << "port[1024+]\n" << std::endl;
}

// 以后用的是./udpserver + port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port)); // new一个对象

    svr->Init(); // 初始化
    svr->Run();  // 跑起来
}

六、udp_socket_client代码编写

1、客户端要绑定吗?

客户端要有具体的ip地址和端口号port的,所以一定是需要绑定的,但是很多人说不需要绑定,这种说法是错误的,因为只是因为客户端不需要用户显示的绑定!一般是由操作系统随机选择的绑定!
解释:一个端口号只能被一个进程绑定,对于server端是如此,对于client端也是如此,而我们计算机上有几百个应用,我们假如说是这前十几个应用把常见的端口号都占了的话,我们后面的应用进程开启来的时候,就不能用这几个常见端口号,打开一次失败一次,试完了这几个常见端口号以后,再用其他冷门的端口号启动就变得很慢,用户自定义的端口号是很粗糙很不安全的,所以由OS操作系统随机的分配更加的安全和有效公平。其实客户端的端口号是多少并不重要,只要能够保证主机上的唯一性即可。

系统什么时候给我绑定端口号的呢?首次发送数据的时候,客户端就进行随机绑定端口号了(即客户端代码跑到sendto的时候)。

2、代码部分

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

void Usage(std::string proc)
{
    std::cout << "\n\rUsages: " << proc << "serverip serverport\n" << std::endl;
}

// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1]; // serverip
    uint16_t serverport = std::stoi(argv[2]); // serverport

    int socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
    if (socketfd < 0)
    {
        std::cout << "socket create error" << std::endl;
        return 1;
    }

    std::string message;
    char buffer[1024];
    while (true)
    {
        // 数据
        std::cout << "Please Enter# ";
        getline(std::cin, message);
        // 给谁发
        struct sockaddr_in server;
        bzero(&server, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        server.sin_addr.s_addr = inet_addr(serverip.c_str());
        
        // 发送数据 -- 把数据发送到socketfd文件中,并将server信息提炼出来发送给server,可以理解成唤醒server
        socklen_t len = sizeof(server);
        sendto(socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);

        // 收数据 -- 从socket文件中的数据拿出来到buffer中,并将收到的对方的个人信息进行保存到temp中
        struct sockaddr_in temp;
        socklen_t len2 = sizeof(temp);
        ssize_t n = recvfrom(socketfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len2);
        if (n > 0)
        {
            buffer[n] = 0;
            // 打印数据
            std::cout << buffer << std::endl;
        }
    }

    close(socketfd);
    return 0;
}

3、C/S逻辑

在这里插入图片描述

4、效果展示

在这里插入图片描述

七、改进代码(解耦合)

1、使用functional改进

在这里插入图片描述

2、改进使用linux命令(popen)

介绍127.0.0.1本地环回地址:
通常用来进行本地测试的。

bool SafeCheck(const std::string& cmd)
{
    std::vector<std::string> word_key = {
        "rm",
        "top",
        "cp",
        "yum",
        "while",
        "kill",
        "unlink"
        "uninstall",
        "top"
    };
    for (auto &word : word_key)
    {
        auto pos = cmd.find(word);
        if (pos != std::string::npos)
        {
            return false;
        }
    }
    return true;
}

std::string ExcuteCommand(const std::string& cmd)
{
    // 做一个保护
    if (!SafeCheck(cmd)) return "bad man";
    
    FILE* fp = popen(cmd.c_str(), "r"); // 管道创建好,子进程创建好,子进程通过管道放到父进程
    if (nullptr == fp)
    {
        perror("popen failed");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while (true)
    {
        char* ok = fgets(buffer, sizeof(buffer), fp); // 写到buffer缓冲区中
        if (ok == nullptr)
        {
            break;
        }
        result += buffer;
    }
    pclose(fp);
    return result;
}

在这里插入图片描述

3、windows与linux进行数据收发

互通代码及成果展示

4、udp简易聊天室

udp简易聊天室

八、地址转换函数

1、介绍

字符串转in_addr的函数:
在这里插入图片描述

in_addr转字符串的函数:
在这里插入图片描述

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

2、关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
在这里插入图片描述
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
在这里插入图片描述

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void* Func1(void* p) {
 struct sockaddr_in* addr = (struct sockaddr_in*)p;
 while (1) {
 char* ptr = inet_ntoa(addr->sin_addr);
 printf("addr1: %s\n", ptr);
 }
 return NULL;
}
void* Func2(void* p) {
 struct sockaddr_in* addr = (struct sockaddr_in*)p;
 while (1) {
 char* ptr = inet_ntoa(addr->sin_addr);
  printf("addr2: %s\n", ptr);
 }
 return NULL;
}
int main() {
 pthread_t tid1 = 0;
 struct sockaddr_in addr1;
 struct sockaddr_in addr2;
 addr1.sin_addr.s_addr = 0;
 addr2.sin_addr.s_addr = 0xffffffff;
 pthread_create(&tid1, NULL, Func1, &addr1);
 pthread_t tid2 = 0;
 pthread_create(&tid2, NULL, Func2, &addr2);
 pthread_join(tid1, NULL);
 pthread_join(tid2, NULL);
 return 0;
}

在这里插入图片描述

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2022horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值