当前位置: 首页 > article >正文

【Linux网络】Linux网络编程套接字,UDP与TCP

📝个人主页🌹:Eternity._
⏩收录专栏⏪:Linux “ 登神长阶 ”
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

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

❀Linux网络编程套接字

  • 📒1. 端口号
  • 📜2. 初识TCP协议与UDP协议
  • 📝3. 网络字节序
  • 📚4. socket编程
    • 🌄socket 常见API
    • 🌄sockaddr结构
    • 🌄封装 sockaddr_in
    • 🌄封装 Udp_Socket
    • 🌄封装 TCP_Socket
  • 🌞5. 进程守护
  • 🌙6. TCP协议通讯流程
  • ⭐7. TCP 和 UDP 对比
  • 📖8. 总结


前言:在当今这个信息技术日新月异的时代,网络编程已成为连接世界、构建各类互联网应用不可或缺的一部分。而Linux,作为开源操作系统的典范,其强大的网络功能和灵活性,为开发者们提供了一个广阔而深入的实践平台。在众多网络编程技术中,套接字(Socket)编程无疑是核心与基石,它不仅支撑着Web服务、即时通讯、在线游戏等日常应用,还是实现分布式系统、云计算服务的关键技术之一。

在套接字编程的世界里,UDP(用户数据报协议)与TCP(传输控制协议)如同双生子,各自以其独特的优势占据着不同的应用场景。TCP,以其可靠的连接导向、顺序传输和错误校正机制,成为了追求数据传输完整性和稳定性的不二之选,如文件传输、远程登录等场景;而UDP,则以其无连接、快速传输和较小的开销,在实时性要求高、对丢包容忍度较大的场合,如视频流、在线游戏等中大放异彩。

本文旨在深入探讨Linux环境下,如何通过套接字编程技术,驾驭UDP与TCP这两种强大的网络传输协议,从零开始构建基础的网络通信能力。

让我们一同踏上这段探索之旅,揭开Linux网络编程的神秘面纱,领略UDP与TCP的魅力所在,共同构建更加智能、互联的世界!


📒1. 端口号


概念:

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

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

注意:端口号和进程ID都可以唯一表示一个进程, 但是一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

源端口号和目的端口号:

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号

简单来说就是 “数据是哪个发的, 最后要发给谁


📜2. 初识TCP协议与UDP协议


TCP(Transmission Control Protocol 传输控制协议):

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

UDP(User Datagram Protocol 用户数据报协议):

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

📝3. 网络字节序


网络字节序(Network Byte Order),也称为网络字节顺序,是协议中规定好的一种数据表示格式。它用于在计算机网络中进行数据通信时,统一数据的字节顺序,确保数据在不同主机之间传输时能够被正确解释。

因为内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,为了定义出网络数据流的地址,我们引入了网络字节序

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

在这里插入图片描述

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

在这里插入图片描述

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

📚4. socket编程


🌄socket 常见API


常见通用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);

在这里插入图片描述

Udp中常见API:

// 函数用于在面向数据报的套接字(如UDP套接字)上发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

在这里插入图片描述

// 用于从套接字接收数据的方法,特别是在使用UDP协议进行数据传输时
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

在这里插入图片描述

Tcp中常见API:

// 开始监听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);

在这里插入图片描述

🌄sockaddr结构


sockaddr是一个在C语言网络编程中使用的数据结构,主要用于表示套接字地址。它是一个通用的结构体,能够用于表示不同类型的套接字地址,如IPv4、IPv6等

在这里插入图片描述

  • 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结构体指针做为参数

sockaddr 结构:

struct sockaddr
{
	__SOCKADDR_COMMON(sa_); /* Common data: address family and length. */
	char sa_data[14]; /* Address data. */
};

sockaddr_in 结构:

/* Structure describing an Internet socket address */
struct sockaddr in
{
	__SOCKADDR_COMMON(sin_); /* Port number. */
	in_port_t sin_port;	/* Internet address. */
	struct in_addr sin_addr;
	
	/* Pad to size of`struct sockaddr'.  */
	unsigned char sin_zero[sizeof(struct sockaddr) -
	SOCKADDR_COMMON_SIZE - 
	sizeof(in_port_t)-
	sizeof(struct in_addr)];
};

in_addr结构:

/* Internet address. */
typedef uint32_t in_addr_t;
struct int_addr
{
	in_addr_t s_addr;
};

🌄封装 sockaddr_in


我们在进行套接字编程时,难免会多次用到一些转换,我们不妨将他们封装起来,让我们的代码变得不那么复杂,从而更容易获取我们想要的数据

#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
        :_addr(addr)
    {
    	// 将网络字节流转换成字符串
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    
    string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    string PrintDebug()
    {
        string info = _ip;
        info += ":";
        info += to_string(_port);

        return info;
    }
	
	// 判断是否是同一个地址
    bool operator== (const InetAddr &addr)
    {
        return _ip == addr._ip && _port == addr._port;
    }

    const sockaddr_in &GetAddr()
    {
        return _addr;   
    }

    ~InetAddr()
    {}
private:
    string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

🌄封装 Udp_Socket


Udp_Server.hpp固定格式:

class UdpServer:public nocopy
{
public:
    UdpServer(uint16_t port)
        :_port(port)
        ,_sockfd(defaultfd)
    {}

    void Init()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket err, %d : %s", errno, strerror(errno));
            exit(Socket_Err);
        }
        lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);

        // 绑定,指定网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 初始化
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // 默认为 0
        // local.sin_addr.s_addr = inet_addr(_ip.c_str());

        // 结构填写完整,转到内核
        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n != 0)
        {
            lg.LogMessage(Fatal, "socket err, %d : %s", errno, strerror(errno));
            exit(Bind_Err);
        }

    }
    void Start()
    {
        // 服务器一直运行
        char buffer[defaultsize];
        for(;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 不能乱写
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                // Server
            }
        }
    }

    ~UdpServer()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    uint16_t _port;
    int _sockfd;
};

在这里插入图片描述

Udp实现聊天室功能

🌄封装 TCP_Socket


Tcp_Server.hpp固定格式:

class Tcp_Server : public nocopy // 防拷贝
{
public:
    Tcp_Server(uint16_t port, bool isrunning = false)
        : _port(port), _isrunning(isrunning)
    {
    }

    void Init()
    {
        // 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            lg.LogMessage(Fatal, "socket err, %d : %s", errno, strerror(errno));
            exit(Socket_Err);
        }
        lg.LogMessage(Info, "create socker success, sockfd: %d ... ", _listensock);

        // 固定写法
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 绑定,指定网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);

        // bind
        if (bind(_listensock, CONV(&local), sizeof(local)) != 0)
        {
            lg.LogMessage(Fatal, "bind socket err, %d : %s", errno, strerror(errno));
            exit(Bind_Err);
        }
        lg.LogMessage(Debug, "bind socket success, sockfd: %d ... ", _listensock);

        // 监听 listen
        if (listen(_listensock, default_black_log) != 0)
        {
            lg.LogMessage(Fatal, "listen socket err, %d : %s", errno, strerror(errno));
            exit(Listen_Err);
        }
        lg.LogMessage(Debug, "listen socket success, sockfd: %d ... ", _listensock);
        
    }

    // 通过read, write来进行数据的读写
    void Service(int sockfd)
    {
        char buffer[1024];
        while(true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                cout << "client say# " << buffer << endl;

                string echo_string = "server echo# ";
                echo_string += buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                lg.LogMessage(Info, "client quite ... \n");
                break;
            }
            else
            {
                lg.LogMessage(Error, "read socket err, %d : %s", errno, strerror(errno));
                break;
            }
        }
    }

    void Start()
    {
        _isrunning = true;
        // signal(SIGCHLD, SIG_IGN);
        while (_isrunning)
        {
            // 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensock, CONV(&peer), &len);
            if (sockfd < 0)
            {
                lg.LogMessage(Fatal, "accept socket err, %d : %s", errno, strerror(errno));
                continue;
            }
            lg.LogMessage(Debug, "accept socket success, sockfd: %d ... ", sockfd);

            // 提供服务
            // v1
            Service(sockfd);
            close(sockfd);
        }
    }
    ~Tcp_Server()
    {
    }

private:
    uint16_t _port;
    int _listensock;
    bool _isrunning;
};

在这里插入图片描述

Tcp实现简单英译汉

🌞5. 进程守护


在之前的学习中,head -1我们经常使用,但是机会没机会了解PGID和SID是啥意思,今天我们来正式认识一下

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

我们同时创建的sleep 10000,sleep 20000,sleep 30000,会不会在同一个进程组里面呢?我们可以查询一下

在这里插入图片描述

移动进程组的指令:

查看任务:jobs
将任务放置到前台:fg task_number
将任务放置到后台:ctrl + Zdg task_number

在这里插入图片描述
守护进程:

守护进程是在后台运行的、不受任何终端控制的进程。它们通常用于执行系统级的任务或服务,如系统监控、网络通信、文件服务等。

在这里插入图片描述

封装 Daemon

🌙6. TCP协议通讯流程


TCP协议的客户端/服务器程序的一般流程:
在这里插入图片描述
建立连接的过程:

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

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

断开连接的过程:

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)
  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN;(第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器;(第四次)

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

⭐7. TCP 和 UDP 对比


  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

udp是面向用户数据包,而tcp面向字节流 — 数据和数据是有边界的

  • sendto、recvfrom ->sendto 发了一次,一定对应对端recvform一次 — 面向数据报
  • write->1,10,100->接收方read可能一次就全部收完了,也可能很多次,但是无论次数是多少,和对方发多少无关 — 面向字节流

📖8. 总结


在探索Linux网络编程的浩瀚领域中,UDP与TCP作为两大核心协议,不仅构建了互联网通信的基石,也成为了每一位网络开发者必须掌握的利器。通过这段旅程,我们一同见证了从基础概念到实践应用的华丽蜕变,从最初的套接字创建、绑定、监听,到数据的发送与接收,每一步都充满了挑战与收获。

在结束这篇文章之际,愿每一位读者都能在网络编程的世界里找到自己的位置,用代码编织梦想,用技术照亮未来。让我们携手前行,在Linux网络编程的广阔天地中,共同书写属于我们的辉煌篇章!

在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述


http://www.kler.cn/a/393244.html

相关文章:

  • ubuntu连接orangepi-zero-2w桌面的几种方法
  • 阿里云通义大模型团队开源Qwen2.5-Coder:AI编程新纪元
  • Redis - 集群(Cluster)
  • 深入理解接口测试:实用指南与最佳实践5.0(二)
  • 大数据面试题--kafka夺命连环问(后10问)
  • 智能零售柜商品识别
  • PCB安全电气间距
  • Python网络爬虫与数据采集实战——网络协议与HTTP
  • linux命令详解,存储管理相关
  • 排序算法 -堆排序
  • SQL面试题——奔驰SQL面试题 车辆在不同驾驶模式下的时间
  • 学Linux的第八天
  • 数字IC实践项目(10)—基于System Verilog的DDR4 Model/Tb 及基础Verification IP的设计与验证(付费项目)
  • uniapp 上传 base64 图片
  • ubuntu22.04上手指南(更新阿里源、安装ssh、安装chrome、设置固定IP、安装搜狗输入法)
  • 【二叉搜素树】——LeetCode二叉树问题集锦:6个实用题目和解题思路
  • 除了 Postman,还有什么好用的 API 测试工具吗
  • uni-app中使用 unicloud 云开发平台③
  • C++生成随机数
  • 信用租赁系统的灵活配置与智能化管理助力租赁市场发展
  • 29系统备份与恢复
  • 深入理解 Vue v-model 原理与应用
  • 量化交易系统开发-实时行情自动化交易-4.1.1.A股趋势跟踪交易策略实现
  • 批量缓存模版
  • 基于yolov5的番茄成熟度检测系统,支持图像、视频和摄像实时检测【pytorch框架、python源码】
  • 【前端篇】Node.js 版本管理新选择:Volta,让版本切换更简单