计算机网络:Socket网络编程 Udp与Tcp协议 第一弹

目录

1.IP地址和端口号

[1.1 如何通信](#1.1 如何通信)

[1.2 端口号详解](#1.2 端口号详解)

[1.3 理解套接字socket](#1.3 理解套接字socket)

[2. 网络字节序](#2. 网络字节序)

[3. socket接口](#3. socket接口)

[3.1 socket类型设计](#3.1 socket类型设计)

[3.2 socket函数](#3.2 socket函数)

[3.3 bind函数](#3.3 bind函数)

[4. UDP通信协议](#4. UDP通信协议)

[4.1 UDP服务端类](#4.1 UDP服务端类)

[4.2 Udp服务类InitServer函数](#4.2 Udp服务类InitServer函数)

[4.3 Udp服务类Start函数](#4.3 Udp服务类Start函数)

[4.4 Udp服务主函数](#4.4 Udp服务主函数)

[4.5 Udp客户端编写](#4.5 Udp客户端编写)

[4.6 运行结果](#4.6 运行结果)


往期博客粗粒度讲解TCP/IP协议的内容,包含对IP地址讲解:

计算机网络:TCP/IP网络协议-CSDN博客

1.IP地址和端口号

1.1 如何通信

不管是任何主机都是遵守TCP/IP协议,可以大致分为四层,分别是应用层、传输层、网络层和数据链路层。一般用户打开的进程在应用层。

  • 当张三使用主机打开许多进程,如抖音进程,浏览器进程和微信进程。其中张三使用微信进程时,会向微信的服务器发送信息。
  • 在全网中,IP地址可以用来标识主机,使一台主机具有唯一性。但是,数据不止要千里迢迢传输到微信服务器的主机上,还要将数据传输到服务器主机上特定的进程,由该进程处理数据,再返回给客户端主机。
  • 因此,传输数据不是目的,而是手段,处理数据才是目的。端口号就是用来标识一台主机上的某个进程。

IP地址由IPv4和IPv6两个版本,主要讲解IPv4。IPv4版本中,IP地址是由32位二进制组成,占4个字节的整数。通常使用点分十进制表示,如:192.168.2.1。

端口号是一个2字节,16二进制组成的整数。通常一个主机上,一个端口号只能被一个进程占用。

IP地址+端口号就可以表示全网中某一台主机上的一个进程

1.2 端口号详解

端口号是一个2字节,16二进制组成的整数。通常一个主机上,一个端口号只能被一个进程占用。

  • 0 - 1023是知名端口号。HTTP,FTP,SSH,SMTP等应用层协议所占有,且它们的端口号都是固定的。
  • 1024 - 65535端口号,可由操作系统动态分配给客户端程序。

当客户端通过网络发送信息给服务端,到达服务端主机的传输层。服务端主机会获取该消息的报头信息,获取端口号交给应用层中的进程。

假设操作系统会创建一个端口号哈希表,哈希表中映射了端口号和进程的pcb。操作系统获取目的端口号,就可以从哈希表中找到目的进程。进程中有文件描述符,以此找到文件缓冲区。操作系统再把收到的数据拷贝到缓冲区中,就可以交由进程处理数据。这是一种方案。

1.3 理解套接字socket

通过前面的讲述,我们知道IP+port可以表示全网中唯一的一个进程。

本质上,网络通信时两个进程代表人进行通信。而源IP地址、源端口号、目的IP地址和目的端口号,就可以标识全网中唯二的进程。

所以,我们将IP地址+port端口号,称之为socket,即套接字。

2. 网络字节序

多字节内存数据存储在内存中,会有存储顺序的问题。按照不同的存储顺序,可以分为大端字节序存储小端字节序存储,即大小端之分。

  • 大端模式,低位字节内容保存在高地址处,高位字节内容保存在低地址处。小端模式则相反。
  • 如把0x11223344写入到地址位0x1000处内存中,其中0x44是低位字节数据,0x11是高位字节数据。

源主机通常将发送缓冲区的数据按内存地址低到高的顺序发出。目标主机接收发送的数据,也是按照内存地址低到高的顺序进行接收。

  • 如果大小端主机不做统一,可能发送数据是0x11223344,但是接收数据变为0x44332211,造成数据混乱。
  • 所以,TCP/IP协议规定,网络数据流应采用大端低字节流,即低地址处存放高位字节内容。

上面的库函数可以做到网络字节序和主机字节序的相互转换。

  • 四个库函数中,h表示主机,n表示网络,l表示32位长整数,s表示16位短整数。
  • 其中htonl函数,可以将32位长整数从主机字节序转换成网络字节序,通常转换IP地址。而htons,转换的是16的短整数,用于端口号转换。

IP地址中,应用层通常以点分十进制展现,如"192.168.32.1"。但是在网络通信过程中,传输一个点分十进制的IP地址,需要十几个字符,字节数太大,所以IP地址一般会转换成一个32位的整数,大小才4字节。

3. socket接口

3.1 socket类型设计

socket套接字一般划分为三类。

  • 网络socket,一般用于网络间通信。
  • 本地socket,也称为unix 域间套接字,通常用于本地通信。
  • 原始套接字,即raw socket,可以直接作用于网络层,不需要经过传输层的处理。

这么多套接字种类,如果各自做一套接口,必定有大量相似的代码。所以,设计者想设计一种类型,统一所有套接字种类。

如下图所示,sockaddr全称socket address,即套接字地址,是一个套接字结构体类型。

可是在C语言中不能直接支持类型的继承和多态,但是规定不管是种套接字类型,开头的字段是2字节大小的16位地址类型,表示该套接字的种类。这就是做到了类型的继承。

而在第一个地址类型字段后,sockaddr基类有14字节的数据。网络套接字sockaddr_in在16位地址类型后,还有16端口号和32位IP地址。unix域间套接字,作用于本地通信,在16地址类型后,还有108字节的路径名字段。

虽然其他套接字结构体可能与sockaddr结构体内部字段字节数不同,但是可以通过类型强制转换成sockaddr结构体类型,统一用于socket接口函数中。

下面是linux2.4.5内核源代码,sockaddr中第一字段是地址类型,而sa_family_t就是无符号16位短整数。

网络socket结构体是sockaddr_in,第二个字段也是个无符号16位短整数的端口号。第三个字段是IP地址,而in_addr结构体中,有一个无符号32位长整数。

3.2 socket函数

socket函数用于创建套接字,该函数有三个参数。第一个参数指定通信协议族,如果要进行网络通信其中AF_INET表示网络通信。

第二个参数是指定套接字协议,传输层协议有Udp和Tcp协议。

  • TCP协议的特点是有连接,具有可靠性,面向字节流,全双工。
  • UDP协议的特点是无连接,不可靠,面向数据报。

第三个参数一般前两个参数设置完后,可以直接传0进去。

如果调用socket函数成功,返回值是一个新的文件描述符。如果失败,会返回一个-1。这个文件描述符就可以作为接受和发送信息的缓冲区。

3.3 bind函数

bind函数是一个套接字和网络地址以及端口号绑定起来。

第一个参数是socket函数返回的文件描述符。第二个参数是sockaddr结构体指针,一般使用网络通信,会传sockaddr_in结构体指针,需要进行类型强制转换,第三个参数该结构体的字节数。

4. UDP通信协议

UDP(用户数据报协议)是一种简单的面向数据报的通信协议。下面我们写一个简单的Udp协议的通信服务,客户端向服务器发送消息,服务器接受消息并把接受的消息回显给客户端。

4.1 UDP服务端类

下面是UdpServer.hpp文件的内容,主要把服务端描述成一个UdpServer类。并且初始化一下服务端的端口号,还有其他的字段。

cpp 复制代码
#ifndef __UDP_SERVER__HPP
#define __UPD_SERVER__HPP

#include <iostream>
#include <string>
#include <cstring>
#include <memory>
#include <string.h>

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


const static int gsockfd = -1;

using namespace LogModule;

class UdpServer
{
public:
    UdpServer(const uint16_t port = gdefaultport)
        :_sockfd(gsockfd)
        ,_port(port)
        ,_isrunning(false)
    {}

    ~UdpServer()
    {
        if (_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    uint16_t _port;  //服务器端口号
    bool _isrunning; //服务器运行状态
};

#endif

4.2 Udp服务类InitServer函数

下面是UdpServer服务端类中的成员函数InitServer的代码,InitServer函数用于初始化Udp服务。

  • 首先使用socket函数创建一个套接字,第一个参数传AF_INET,表示网络通信。第二个参数传SOCK_DGRAM,表示使用Udp协议,第三个参数默认传0即可。
  • 接着,填充网络信息。定义一个sockaddr_in结构体,该结构体内部有三个字段需要填充。第一个字段表示什么通信,填AF_INET,表示网络通信。第二字段填端口号,但是得使用htons函数将主机字节序转换成网络字节序,变成大端模式。
  • 第三个字段一般来说需要填写机器的IP地址,但是作为服务器,可能客户端发来的消息需要多台服务器处理不同的信息,如果服务器填写固定的IP地址,那么客户端接受服务器的消息后,只能返回给一台服务器。所以服务器的sin_addr中的s_addr一般设置INADDR_ANY。
cpp 复制代码
// ......
const std::string gdefaultip = "127.0.0.1";
const static uint16_t gdefaultport = 8080;

class UdpServer
{    
public:
    // ......
    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            std::cout << "socket:" << strerror(errno) << std::endl;
            exit(1);
        }
        std::cout << "socket success, sockfd is: " << _sockfd << std::endl;
        
        // 2、填充网络信息,
        // 2.1 没有把socket信息,设置进如内核中,只是填充了结构体!
        struct sockaddr_in loacl;
        memset(&loacl, 0, sizeof(loacl));
        loacl.sin_family = AF_INET;
        loacl.sin_port = ::htons(_port); //要被发送给对方的,即要发送到网络中!
        loacl.sin_addr.s_addr = INADDR_ANY;
        //loacl.sin_addr.s_addr = ::inet_addr(_ip.c_str()); //1. 点分十进制转为整数 2. 转成网络字节序

        // 2.2 bind 设置如内核中
        int n = ::bind(_sockfd, (struct sockaddr*)&loacl, sizeof(loacl));
        if(n < 0)
        {
            std::cout << "bind: "<< errno << " " <<strerror(errno) << std::endl;
            exit(2);
        }
        std::cout << "bind success" << std::endl;
    }
    // ......
private:
    int _sockfd;
    uint16_t _port;  //服务器端口号
    bool _isrunning; //服务器运行状态
};

4.3 Udp服务类Start函数

下面是Udp的Start成员函数的代码,用于接收客户端的信息,并回显给客户端。

首先将服务状态设置为真。再写个死循环,一般服务端的程序是不轻易下线的。只有while结束,服务状态再设置成假。

定义一个sockaddr_in结构体变量,用于接收客户端的IP地址和端口。读取网络传过来的信息,可以使用recvfrom函数。下面的recvfrom函数原型,跟read函数类似。

  • 第一个参数填socket函数创建的文件描述符。第二个参数填一个指针类型的变量,用于接受传过来的信息,我们默认传过来的是字符串。第三个参数是该指针变量指向空间的大小。第四个参数是个标记位,填写0即可。
  • 第五个参数是sockaddr结构体指针类型,需要填入sockaddr_in变量,并强转成sockaddr类指针。第六个参数是类型socklen_t,socklen_t就是一个无符号的32位整数类型,里面要记录sockaddr_in类型的大小。
  • 如果该函数调用成功,返回值是一个大于0的整数,表示接受信息的字节数大小。如果失败返回-1。如果返回0,表示读取到文件末尾,或者客户端已关闭

接着n大于0,表示接受到有效信息。需要在下标为n的位置加上反斜杠0。调用ntohs函数可以将网络字节序转换成主机字节序,获取端口号。且inet_ntoa函数可以将sockaddr_in中无符号整数IP地址,转换成点分十进制的字符串。

最后拼接一下字符串,使用sendto函数,转发信息。sendto函数的前四个参数跟recvfrom函数一样。

  • 第五个参数需要传客户端的sockaddr_in类结构体变量,里面包含客户端的IP地址和端口号。
  • 最后一个参数也是socklen_t类型参数,里面填上dest_addr变量实际的大小即可。
cpp 复制代码
class UdpServer
{    
public:
    // ......
    void Start()
    {
        _isrunning = true;
        while(true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); //必须设定
            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)(&peer), &len);
            if (n > 0)
            {
                // 在后面字符串后面加上反斜杠0,以便于输出
                inbuffer[n] = 0;

                // 1.消息内容 && 2.谁发给我的
                uint16_t clientport = ::ntohs(peer.sin_port);
                std::string clientip = ::inet_ntoa(peer.sin_addr);

                std::string clientinfo = clientip + ":" + std::to_string(clientport) + "# " + inbuffer;
                std::cout << clientinfo << std::endl;

                std::string echo_str = "echo# ";
                echo_str += inbuffer;
                ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr*)(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }
    // ......
private:
    int _sockfd;
    uint16_t _port;  //服务器端口号
    bool _isrunning; //服务器运行状态
};

4.4 Udp服务主函数

Udp服务端只需要绑定一个端口号,所以在传命令号时,强制传两个参数。其中需要把命令行第二参数转换成整数。

然后再使用智能指针初始化服务类变量,运行InitServer函数和Start函数即可。

cpp 复制代码
#include "UdpServer.hpp"

// ./server_udp localport
int main(int argc, char *argv[])
{   
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        return 3;
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);
    svr_uptr->InitServer();
    svr_uptr->Start();

    return 0;
}

4.5 Udp客户端编写

客户端主代码编写时,可以在调用该程序的命令行参数后,加上访问的IP地址和端口号。其中要把端口号转换成整数类型。

实现网络通信,第一个也是创建socket套接字。然后,再填充服务端的网络信息。

但是不需要填写自己的网络信息,并且不用bind函数进行绑定。在客户端第一次发消息时,操作系统会动态绑定端口号,而IP地址是传输层的下一层网络层。

再下来就是使用sendto函数发消息,recvfrom函数接受消息。

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        Die(ExitError::USAGE_ERR);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1.创建socket
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(ExitError::SOCKET_ERR);
    }

    // 1.1 填充server信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = ::inet_addr(serverip.c_str());


    // 2. clientdone
    while(true)
    {
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);
        // client必须要也要有自己的ip和端口号!但是客户端,不需要自己显示的调用bind!!
        // 而是,客户端首次sendto消息的时候,由OS自动进行bind
        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&temp), &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

4.6 运行结果


创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!

相关推荐
reddingtons1 小时前
在 Ubuntu 下通过 Docker 部署 Mastodon 服务器
服务器·ubuntu·docker
予安灵5 小时前
《白帽子讲 Web 安全:点击劫持》
前端·网络·安全·web安全·网络攻击模型·安全威胁分析·点击劫持
屁股割了还要学5 小时前
【计算机网络入门】初学计算机网络(五)
学习·计算机网络·考研·青少年编程
屁股割了还要学5 小时前
【计算机网络入门】初学计算机网络(七)
网络·学习·计算机网络·考研·青少年编程
cdprinter6 小时前
信刻光盘安全隔离与信息交换系统让“数据摆渡”安全高效
网络·安全·自动化
一天八小时7 小时前
计算机网络学习————(五)TCP/IP学习
学习·tcp/ip·计算机网络
winkel_wang7 小时前
Centos7服务器防火墙设置教程
linux·运维·服务器
lihan_freak7 小时前
计算机网络---TCP三握四挥
网络协议·tcp/ip·计算机网络
镜中人★8 小时前
中科大计算机网络笔记第一章1.8 互联网历史笔记
网络