Linux网络-------1.socket编程基础---(TCP-socket)

1.tcp和udp的区别

UDP(User Datagram Protocol)和 TCP(Transmission Control Protocol)是两种最常用的传输层协议,它们在可靠性、连接方式、速度和应用场景等方面有显著区别。以下是它们的核心差异:


1. 连接方式

  • TCP:面向连接(Connection-Oriented)

    • 通信前需通过 三次握手 建立连接,结束时通过 四次挥手 断开连接。
    • 确保双方准备好通信后才传输数据。
    • 示例:打电话前需先拨通,结束后要挂断。
  • UDP:无连接(Connectionless)

    • 直接发送数据,无需预先建立连接。
    • 示例:寄信时无需确认收件人是否在家。

2. 可靠性

  • TCP:可靠传输

    • 通过 确认应答(ACK)超时重传流量控制 等机制确保数据不丢失、不重复、按序到达。
    • 适合对数据准确性要求高的场景(如文件传输、网页浏览)。
  • UDP:不可靠传输

    • 不保证数据是否到达、是否按序、是否重复。
    • 适合对实时性要求高、能容忍少量丢失的场景(如视频流、游戏)。

3. 数据传输方式

  • TCP :基于字节流(Byte Stream)

    • 数据被视为连续的字节流,无明确边界。
    • 应用层需自行处理消息边界(如添加分隔符)。
  • UDP :基于数据报(Datagram)

    • 每个数据包是独立的,有明确边界。
    • sendtorecvfrom 每次收发一个完整的数据包。

4. 速度和效率

  • TCP:速度较慢,开销大

    • 需要维护连接状态、重传丢失数据、保证顺序,引入额外延迟。
    • 首部较大(20字节以上),包含控制字段(如序列号、ACK号)。
  • UDP:速度快,开销小

    • 无连接管理、重传等机制,实时性高。
    • 首部仅8字节(源端口、目的端口、长度、校验和)。

5. 拥塞控制

  • TCP:动态调整发送速率

    • 通过 慢启动拥塞避免 等算法避免网络过载。
    • 公平性高,但可能降低突发流量的速度。
  • UDP:无拥塞控制

    • 持续以固定速率发送数据,可能加剧网络拥堵。
    • 适合实时应用(如直播),但需应用层自行优化。

6. 应用场景

TCP UDP
网页浏览(HTTP/HTTPS) 视频流(Zoom、YouTube)
文件传输(FTP、SFTP) 在线游戏(王者荣耀、吃鸡)
电子邮件(SMTP) DNS 查询
远程登录(SSH) VoIP(微信语音、Skype)
数据库访问(MySQL) 广播/多播(如DHCP)

7. 头部对比

字段 TCP 头(20+字节) UDP 头(8字节)
源端口/目的端口 ✔️ ✔️
序列号/确认号 ✔️(保证顺序)
数据偏移/标志位 ✔️(控制连接状态)
窗口大小 ✔️(流量控制)
校验和 ✔️ ✔️(可选)

8. 代码示例差异

TCP 服务端 :需 listen() + accept()

c 复制代码
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
recv(client_fd, buffer, sizeof(buffer), 0);

UDP 服务端 :直接 recvfrom()

c 复制代码
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &len);

总结

  • 选择 TCP:需要可靠传输、数据完整性(如支付、文件下载)。
  • 选择 UDP:需要低延迟、能容忍丢包(如直播、实时竞技游戏)。
  • 混合使用:部分应用同时使用两者(如QUIC协议结合了UDP的速度和TCP的可靠性)。

2.tcp协议函数详细介绍

1.listen函数(初始化)-------这个基本上只在服务端设置!!

  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客⼾端处于连接等待状态,如果接收到更多的连接请求就忽略,这⾥设置不会太⼤(⼀般是5)
  • listen()成功返回0,失败返回-1;

在初始化上-------------------------比udp多了一条设置listen状态!!!!

2.accept函数----获取链接!!

  • 返回值是文件描述符!!!!为什么listensockfd已经是文件描述符了,还要再建立一个呢??????

答案·是:

1.listensockfd只用来提供accept函数的获取链接功能和建立listen !!!!!!,并不提供其他服务

2.而accept函数的返回的文件描述符---sockfd来提供服务

  • sockfd来提供服务,而_listensockfd只提供建立链接和接收!!!

3.read函数----从其他端口读取内容!!!

  • 还记得read系统调用吗,没座!就是参数为文件描述符的系统调用,之前用于读取进程的内容
  • 现在网络也是文件,而且还有accept函数获取的sockfd,这样,不就可以通过read函数来读取对方端的内容了吗!!!!!!
  • 和之前udp的recv函数功能一致,不过这次直接使用了系统调用read!!!

4.write函数----从其他端口写入内容!!!

不过多解释,和上文一致,都是系统调用!!!

5.connect函数----链接服务端!!!!

3.基于tcp协议构建多客户端的翻译字典

Common.hpp:

cpp 复制代码
#pragma once

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

enum ExitCode
{
    OK = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR
};//在 C/c++ 中,枚举器会直接暴露在外层作用域(需直接使用 OK 而非 ExitCode::OK)。

class NoCopy
{
public:
    NoCopy(){}
    ~NoCopy(){}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator = (const NoCopy&) = delete;
};//它的作用是 禁止对象的拷贝构造和拷贝赋值,即让这个类的对象不能被复制。这是 C++11 引入的 = delete 特性的典型应用。

#define CONV(addr) ((struct sockaddr*)&addr)//----强制类型转换

InetAddr.hpp:网络地址和主机地址之间进行转换

cpp 复制代码
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr(){} //参数是ip结构体,初始化的主机的ip和端口
    InetAddr(struct sockaddr_in &addr) : _addr(addr)//这是客户端/服务端使用recvfrom函数之类的获取的另一方的ip结构体,保存为主机版/------收取信息并保存
    {
        // 网络转主机
        _port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
        // _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));//这行代码的作用是 将二进制格式的 IPv4 地址(_addr.sin_addr)转换为人类可读的点分十进制字符串形式(如 "192.168.1.1"),并存储到 ipbuffer 中。
        _ip = ipbuffer;
    }
    
    //参数是ip和端口,初始化的是ip结构体!!!!
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)//-----------这是服务端/客户端使用sendto之类函数使用的---发送信息
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);//这行代码的作用是 将人类可读的点分十进制IPv4字符串(如 "192.168.1.1")转换为二进制网络字节序格式,并存储到 _addr.sin_addr 中,以便用于网络通信(如 bind、connect 等函数)。
        _addr.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO

    }

    //初始化的是ip结构体!!!!
    InetAddr(uint16_t port) :_port(port),_ip()//-----------这是服务端使用的主机转网络----服务端使用bind函数时获取并初始化主机地址使用的函数
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));//结构体清空内存!!!!
        _addr.sin_family = AF_INET;//统一为AF_INET
        _addr.sin_addr.s_addr = INADDR_ANY;//设置为0,-----只要端口号符合,不管ip地址是多少,服务端直接接收!!!!!
        _addr.sin_port = htons(_port);//使用htons函数转为网络序列!!!!!!
    }
    uint16_t Port() { return _port; }//获取端口号
    std::string Ip() { return _ip; }//获取ip
    const struct sockaddr_in &NetAddr() { return _addr; } //获取sockaddr_in类型的结构体
    const struct sockaddr *NetAddrPtr()//获取sockaddr类型的结构体------便于bind函数进行绑定!!!!
    {
        return CONV(_addr);//#define CONV(addr) ((struct sockaddr*)&addr)------------把CONV(addr)-------翻译为((struct sockaddr*)&addr),就是类型转换的意思
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);//获取ip结构体的大小,也是为了给bind函数提供参数!!!
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port); //返回地址名---ip地址加端口号!!!!!
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

Dict.hpp:字典翻译类!!!

cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

using namespace LogModule;

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path)
    {
    }
    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            // "apple: 苹果"
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容: " << line;
                continue;
            }

            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }

        in.close();
        return true;
    }
    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
        return iter->second;
    }
    ~Dict()
    {
    }

private:
    std::string _dict_path; // 路径+文件名
    std::unordered_map<std::string, std::string> _dict;
};

dictionary.txt:

cpp 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好



run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

Log.hpp:----日志类不再黏贴!!!

makefile:

cpp 复制代码
.PHONY:all
all:tcpclient tcpserver

tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++17
tcpserver:TcpServer.cc
	g++ -o $@ $^ -std=c++17 -lpthread

.PHONY:clean
clean:
	rm -f tcpclient tcpserver

TcpClient.cc:客户端

cpp 复制代码
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"

        
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}

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

    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(SOCKET_ERR);
    }
    // 2. bind吗??需要。显式的bind?不需要!随机方式选择端口号
    // 2. 我应该做什么呢?listen?accept?都不需要!!!------------因为是客户端!!!!只需要链接即可!!
    // 3. 直接申请链接服务端即可!!!
    InetAddr serveraddr(serverip, serverport);
    int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());//connect函数链接!!!!!
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    // 3. echo client
    while(true)
    {
        std::string line;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());//

        char buffer[1024];
        ssize_t size = read(sockfd, buffer, sizeof(buffer)-1);
        if(size > 0)
        {
            buffer[size] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

TcpServer.cc:服务端

cpp 复制代码
#include "Command.hpp"
#include "TcpServer.hpp"
#include "Dict.hpp"

std::string defaulthandler(const std::string &word, InetAddr &addr)
{
    LOG(LogLevel::DEBUG) << "回调到了defaulthandler";
    std::string s = "haha, ";
    s += word;
    return s;
}

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " port" << std::endl;
}

// 远程命令执行的功能!
// ./tcpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);

    Enable_Console_Log_Strategy();

    // // 1. 翻译模块
     Dict d;
     d.LoadDict();
    // 1. 命令的执行模块
    //Command cmd;
    //  std::string Execute(const std::string &cmd, InetAddr &addr)

    1.//std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,
                                                                     //std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));
    // std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&cmd](const std::string &command, InetAddr &addr)
    //                                                               { return cmd.Execute(command, addr); });
    ;
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&d](const std::string &word, InetAddr &addr){
         return d.Translate(word, addr);
     });

    tsvr->Init();
    tsvr->Run();

    return 0;
}

TcpServer.hpp:

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;

// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&, InetAddr &)>;

const static int defaultsockfd = -1;
const static int backlog = 8;

class TcpServer : public NoCopy//继承nocopy类来保证服务器不会被拷贝!!!!
{
public:
    TcpServer(uint16_t port, func_t func) : _port(port),
                                            _listensockfd(defaultsockfd),
                                            _isrunning(false),
                                            _func(func)
    {
    }
    void Init()
    {
        // signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法
        // 1. 创建套接字文件
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);////创建套接字---------第一个和udp一致,都是AF_INET,第二个参数不一样,TCP面向字节流所以是SOCK_STREAM
        if (_listensockfd < 0)                          ////而UDP是SOCK_DRGAM!!!!!!!!
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3

        // 2. bind众所周知的端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3

        // 3. 设置socket状态为listen-------比udp多了一条设置listen状态!!!!`
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3
    }

    class ThreadData
    {
    public:
        ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
        {
        }

    public:
        int sockfd;
        InetAddr addr;
        TcpServer *tsvr;
    };

    // 短服务
    // 长服务: 多进程多线程比较合适
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            // a. n>0: 读取成功
            // b. n<0: 读取失败
            // c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipe
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                // buffer是一个英文单词 or 是一个命令字符串
                buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;

                std::string echo_string = _func(buffer, peer);

                // // 2. 写回数据
                // std::string echo_string = "echo# ";
                // echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";
                close(sockfd);//要使用系统调用把sockfd给关掉,因为,n==0相当于我们还要读,但是对方已经不写了,(读到了文件的结尾)这个时候会造成阻塞,需要把读的通道给关闭!!!!
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";
                close(sockfd);
                break;
            }
        }
    }

    static void *Routine(void *args)//线程获得的工作函数!!!!即入口!!!-----这里为什么要设置成static,因为pthread_create的第三个参数要求是一个函数指针,其类型为:一个接受void*参数并返回void*的函数指针。
    {                               //如果去掉static,那么设置的函数会多出一个this指针(隐含参数),不符合pthread_create要求的函数参数只有一个void*类型,修改成static就不再包含this指针了!!!!!!!!!!!!
        pthread_detach(pthread_self());//分离线程,为什么要分离线程?--------为了不用继续join,如果要join,那么一个时刻就只能有一个客户端访问,那还怎么玩呢?要的就是多个用户(线程)同时进行访问!!!!!所以需要分离线程!!!

        //正是因为是静态成员函数,所以没有办法直接调用service函数(无法直接访问类内部的函数,一因为没有this指针!!!!!!!)
        //需要拿到TcpServer *s才能访问,正好threaddata类里就有这个TcpServer *s-----tsvr!!!可以通过tsvr来访问!!!
        ThreadData *td = static_cast<ThreadData *>(args);//强制转换获得指针
        td->tsvr->Service(td->sockfd, td->addr);//运行service函数!!!!!!!!!!
        delete td;
        return nullptr;
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // a. 获取链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 如果没有连接,accept就会阻塞
            int sockfd = accept(_listensockfd, CONV(peer), &len);//sockfd来提供服务,而_listensockfd只提供建立链接和接收!!!
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();

            // version2: 多线程版本
            ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);

            // version0 -- test version --- 单进程程序 --- 不会存在的!
            // Service(sockfd, addr);

            // version1 --- 多进程版本
            // pid_t id = fork(); // 父进程
            // if(id < 0)
            // {
            //     LOG(LogLevel::FATAL) << "fork error";
            //     exit(FORK_ERR);
            // }
            // else if(id == 0)
            // {
            //     // 子进程,子进程除了看到sockfd,能看到listensockfd吗??
            //     // 我们不想让子进程访问listensock!
            //     close(_listensockfd);
            //     if(fork() > 0) // 再次fork,子进程退出
            //         exit(OK);

            //     Service(sockfd, addr); // 孙子进程,孤儿进程,1, 系统回收我
            //     exit(OK);
            // }
            // else
            // {
            //     //父进程
            //     close(sockfd);

            //     //父进程是不是要等待子进程啊,要不然僵尸了??
            //     pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
            //     (void)rid;
            // }
            // // version2: 多线程版本
            // ThreadData *td = new ThreadData(sockfd, addr, this);
            // pthread_t tid;
            // pthread_create(&tid, nullptr, Routine, td);

            // version3:线程池版本,线程池一般比较适合处理短服务
            // 将新链接和客户端构建一个新的任务,push线程池中
            // ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){
            //     this->Service(sockfd, addr);
            // });
        }
        _isrunning = false;
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensockfd; // 监听socket
    bool _isrunning;

    func_t _func; // 设置回调处理
};
相关推荐
mCell1 小时前
从删库到跑路?这50个Linux命令能保你职业生涯
linux·windows·macos
杰克逊的日记1 小时前
GPU运维常见问题处理
linux·运维·gpu
誰能久伴不乏2 小时前
Linux系统调用概述与实现:深入浅出的解析
linux·运维·服务器
程序员学习随笔2 小时前
Linux进程深度解析(2):fork/exec写时拷贝性能优化与exit资源回收机制(进程创建和销毁)
linux·运维·服务器
mmoyula2 小时前
【RK3568 PWM 子系统(SG90)驱动开发详解】
android·linux·驱动开发
guts°2 小时前
17-VRRP
网络·智能路由器
Jewel Q2 小时前
动态路由协议基础
网络·智能路由器
-SGlow-3 小时前
MySQL相关概念和易错知识点(2)(表结构的操作、数据类型、约束)
linux·运维·服务器·数据库·mysql
代码改变世界ctw3 小时前
Linux内核设计与实现 - 第14章 块I/O层
linux·运维·服务器
宇称不守恒4.03 小时前
2025暑期—06神经网络-常见网络2
网络·人工智能·神经网络