网络编程套接字(一)

个人主页:Lei宝啊 

愿所有美好如期而遇


端口号

我们上网,就是两种动作:1.从远端拉取数据;2.将数据从本地推送到远端。             

我们是用户,平时怎么上网呢?打开抖音app,今日头条等等,将他们打开时,就是启动进程,也就是说,是进程在上网,是进程在从远端服务器上拉取数据到我们本地,我们才可以看到一个个的短视频,一篇篇文章。

我们是客户端,远端服务器是服务端,我们在请求数据时,将数据交给服务器并不是我们的目的,我们的目的是将数据交给服务器的指定服务进程,一个服务器并不止一个进程;同时,我们打开app,也可能不止启动一个进程,服务器将数据发送到我们主机上时,也需要确定要交给哪个进程,就像抖音服务器上拉取的数据,到我们主机上时,不可能交付给今日头条的进程,而是给我们抖音客户端的进程。

那么我们如何确定应该交付给哪个进程?

IP在互联网中唯一确定一台主机,而端口号唯一确定一台主机上的一个进程,所以,IP+port,我们就可以确定互联网中唯一的一个进程。

所以客户端可以确定互联网中唯一的一个进程,服务端也可以,他们之间通信时也就可以互相确定,也就可以唯一的找到彼此(src ip, src port,  dst ip, dst port)。

所以,网络通信的本质实际上就是进程间通信!进程间通信就需要使得进程之间看到同一份公共资源,所以网络通信,进程间看到的公共资源是什么?网络!

那么端口号用来标识一台主机上唯一的一个进程,但是我们在Linux系统部分,知道pid也是唯一标识一个进程的,那么为什么又出来一个端口号,不能直接用pid吗?有这样几个原因:如果使用pid,那么网络部分和系统部分将强耦合,如果将来系统部分不使用pid标识唯一进程,那么网络部分也得改;再一个,每一次进程重新启动时,pid都会改变,不够稳定。所以我们使用端口号,将进程PCB与其关联起来。

而通过ip地址和端口号进行通信,我们叫做socket网络通信。

简单认识TCP协议及UDP协议

这里我们先有一个直观认识:

TCP协议
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流
UDP协议
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报 

可靠传输和不可靠传输体现在,如果网络传输过程中丢包,那么TCP协议会补发,而UDP协议不会,但是这也就意味着UDP协议复杂性注定了低于TCP协议,我们这里对于他们的可靠性不要理解成优缺点,而是理解成特点。 

socket编程接口

UDP

创建socket文件描述符

其实这个接口就类似于创建一个文件,除了默认打开的三个文件,我们打开他,fd==3,就像是对方主机找到我们的主机,通过端口号找到我们的进程,之后通过进程打开的这个套接字文件进行网络通信。 


 绑定socket与网络信息

绑定sockfd与网络信息,网络信息就是struct sockaddr* 指向的结构体,这个结构体还有说法:

socket编程,是有不同种类的,有的是用来进行本地通信的,有的是用来进行跨网络通信的,还有的是用来进行网络管理的这样也就有不同的结构体类型,后来为了方便,就统一了接口,统一了类型,将不同种类的socket进行强制类型转换。

struct sockaddr,就是我们统一的结构体类型,struct sockaddr_in就是用于跨网络通信,struct sockaddr_un就是用于本地通信。

我们来看看他们内部的结构(本地通信这里不做介绍):

既然他们都有16位地址类型这个字段,也就是sa_family,那么sockaddr_in中的这个字段呢?我们看看第一个宏的定义:

也就是将名字进行拼接,传参为sin_,拼接##后的family就是sa_family,整体就是                  sa_family_t sa_family,所以强转以后,OS内部也就可以区分是要进行跨网络通信还是本地通信。

那么双方进行通信时,需要知道各自的ip地址和端口号,换句话说,要经过网络传输,但是,我客户端要怎么知道服务端的ip地址和端口号呢?

网络字节序与主机字节序

我们知道,内存中的多字节数据相对于内存地址有大端小端之分,那么不同的主机他们可能有的是大端,有的是小端,字节序不同的主机通过网络传输将数据传输过去,会出现无法识别的情况,所以也就有了规定,数据在进行网络传输时,需要先将数据转换为大端字节序,在解析数据时,也同样是如此,需要将数据网络数据的大端字节序转换为本主机的字节序。

这项工作需要我们去做吗?不需要:

库函数已经为我们做了这件事,他们的含义分别是:

  • 将无符号32位整数从主机序列转换为网络序列。
  • 将无符号16位整数从主机序列转换为网络序列。
  • 将无符号32位整数从网络序列转换为主机序列。
  • 将无符号16位整数从网络序列转换为主机序列。
代码编写(UDP)
服务端

服务端,我们的大致结构是这样的:

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

int main()
{
    unique_ptr<UdpServer> ptr = make_unique<UdpServer>(2020);
    ptr->Init();
    ptr->Start();
    return 0;
}

首先就是创建服务端,然后进行初始化,最后启动。8080是我们设置的服务端的端口号,端口号范围是0~65535,我们一般使用1024以后的端口号,测试时一般使用127.0.0.1 ip地址,我们称为本地环回,这样测试,数据不会真正通过网卡发送到网络中,而是在主机上走一圈再返回。 

如果是在云服务器上,那么还需要在云服务器的控制台将指定端口打开,否则是无法使用公网ip进行通信的。 

具体实现:

包含了线程池,线程,日志头文件,这些代码在以往的博客中都有,这里不再写出。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <unordered_map>
#include <list>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"
#include "ThreadPool.hpp"
#include <string.h>
#include <queue>
#include "public.hpp"
using namespace std;

class UdpServer
{
private:
    uint16_t _port; // 服务端端口号
    int _sockfd;

    unordered_map<string, uint16_t> usersinfo;
    queue<string> qs;

public:
    UdpServer() {}

    UdpServer(uint16_t port) : _port(port)
    {
    }
    ~UdpServer()
    {
    }

    void Init()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP
        if (_sockfd < 0)
        {
            Log(Fatal, "socket create error");
            exit(SOCK_CREATE_ERROR);
        }
        // socket创建成功

        // 接下来需要将socket与网络信息进行绑定
        struct sockaddr_in server;
        bzero(&server, 0);

        server.sin_addr.s_addr = 0; // 表示绑定本机任意ip地址
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);

        socklen_t len = sizeof(server);
        int bindval = bind(_sockfd, (struct sockaddr *)&server, len);
        if (bindval < 0)
        {
            Log(Fatal, "socket bind error");
            exit(SOCK_BIND_ERROR);
        }
        // 绑定成功
        Log(Info, "socket init success");
    }
    
    void GetInfo()
    {
     
        char buffer[1024]; bzero(buffer, 0);
        struct sockaddr_in client;  bzero(&client, 0);
        
        socklen_t len = sizeof(client);

        while (true)
        {           
            ssize_t realnum = recvfrom(_sockfd, buffer, sizeof(buffer), 
                                   0, (struct sockaddr *)&client, &len);
            if (realnum < 0)
            {
                Log(Error, "get clientinfo error");
                continue;
            }
            
            Analyze ret(client);
            buffer[realnum] = '\0';
            cout << "[server message from: " << ret.getip() 
                        << "]: " << buffer << endl;

            qs.push(buffer);
            usersinfo[ret.getip()] = ret.getport();       
        }
    }

    void SendInfo()
    {
        struct sockaddr_in client;
        bzero(&client, 0);

        while (true)
        {
            if(qs.empty()) continue;
            string str = qs.front();
            qs.pop();
    
            client.sin_family = AF_INET;
           
            socklen_t len = sizeof(client);
            for(auto &e: usersinfo)
            {
                client.sin_port = htons(e.second);
                client.sin_addr.s_addr = inet_addr(e.first.c_str());
                ssize_t val = sendto(_sockfd, str.c_str(), str.size(), 
                                0, (struct sockaddr *)&client, len);
            }
        }
    }

    // 在这里要实现多线程,使用线程池,在全局预先创建好线程池
    // 服务端接受和发送消息方法分开写
    void Start()
    {
        ThreadPool<function<void()>> *Pool = 
                            ThreadPool<function<void()>>::GetPool();

        Pool->ThreadInit();
        Pool->Start();

        function<void()> func1 = bind(&UdpServer::GetInfo, this);
        Pool->Push(func1);

        function<void()> func2 = bind(&UdpServer::SendInfo, this);
        Pool->Push(func2);

        Pool->Wait();
    }
};
客户端

客户端就比较简单一点

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <unordered_map>
#include <list>
#include <string>
#include <arpa/inet.h>
#include "log.hpp"
#include "ThreadPool.hpp"
#include <string.h>
#include "public.hpp"
using namespace std;

class UdpClient
{
private:
    string _ip;
    uint16_t _port;
    int _sockfd;

public:
    UdpClient() {}

    UdpClient(string ip, uint16_t port) : _ip(ip), _port(port)
    {
    }

    void Init()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP
        if (_sockfd < 0)
        {
            Log(Fatal, "socket create error");
            exit(SOCK_CREATE_ERROR);
        }
        // socket创建成功, 客户端初始化完成

        // 客户端不需要进行绑定,在向服务端发送消息时OS自动完成
    }

    void GetInfo()
    {
        char buffer[1024]; bzero(buffer, 0);  
        struct sockaddr_in server; bzero(&server, 0);
        socklen_t len = sizeof(server);

        while (true)
        {               
            ssize_t realnum = recvfrom(_sockfd, buffer, sizeof(buffer), 
                                0, (struct sockaddr *)&server, &len);
            if (realnum > 0)
            {
                Analyze ret(server);
                buffer[realnum] = '\0';
                cout << "[client message from: " << ret.getip() 
                            << "]: " << buffer << endl;
            }    
        }
    }

    void SendInfo()
    {
        struct sockaddr_in goal;
        bzero(&goal, 0);
        goal.sin_family = AF_INET;
        goal.sin_addr.s_addr = inet_addr(_ip.c_str());
        goal.sin_port = htons(_port); // port主机序列转网络序列

        string str;
        while (true)
        {
            cout << "Please Enter# ";
            getline(cin, str);

            // 将消息发送给服务端
            ssize_t val = sendto(_sockfd, str.c_str(), str.size(), 
                        0, (struct sockaddr *)&goal, sizeof(goal));
            if (val < 0)
            {
                Log(Error, "send clientinfo error");
		        perror("sendto:");
                continue;
            }
        }
    }

    void Start()
    {
        ThreadPool<function<void()>> *Pool = 
                            ThreadPool<function<void()>>::GetPool();

        Pool->ThreadInit();
        Pool->Start();

        function<void()> func1 = bind(&UdpClient::GetInfo, this);
        Pool->Push(func1);

        function<void()> func2 = bind(&UdpClient::SendInfo, this);
        Pool->Push(func2);

        Pool->Wait();
    }
};

之后就可以运行了,服务端直接./server,客户端./client ip port即可。


如果有需要所有文件,可以后台私信我,看到会回。

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-19 09:50:01       4 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-19 09:50:01       5 阅读
  3. 在Django里面运行非项目文件

    2024-06-19 09:50:01       4 阅读
  4. Python语言-面向对象

    2024-06-19 09:50:01       4 阅读

热门阅读

  1. MySQL-DML-约束

    2024-06-19 09:50:01       20 阅读
  2. 研导AI写作:辅助创作的未来伙伴

    2024-06-19 09:50:01       18 阅读
  3. vue3基础

    2024-06-19 09:50:01       20 阅读
  4. C++ 设计模式

    2024-06-19 09:50:01       19 阅读
  5. 19、架构-虚拟化容器

    2024-06-19 09:50:01       15 阅读
  6. python 数据清洗基础教程

    2024-06-19 09:50:01       16 阅读
  7. MongoDB基础知识

    2024-06-19 09:50:01       15 阅读