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

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. 应用场景

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

7. 头部对比

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

8. 代码示例差异

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

int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
recv(client_fd, buffer, sizeof(buffer), 0);

UDP 服务端:直接 recvfrom()

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:

#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:网络地址和主机地址之间进行转换

#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 -> 点分十进制的字符串风格的IPchar 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; }//获取ipconst 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:字典翻译类!!!

#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:

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

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

makefile:

.PHONY:all
all:tcpclient tcpservertcpclient:TcpClient.ccg++ -o $@ $^ -std=c++17
tcpserver:TcpServer.ccg++ -o $@ $^ -std=c++17 -lpthread.PHONY:clean
clean:rm -f tcpclient tcpserver

TcpClient.cc:客户端

#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. 创建socketint 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 clientwhile(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:服务端

#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:

#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_STREAMif (_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: 对端把链接关闭了,读到了文件的结尾 --- pipessize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// buffer是一个英文单词 or 是一个命令字符串buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1LOG(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; // 监听socketbool _isrunning;func_t _func; // 设置回调处理
};
http://www.dtcms.com/a/300171.html

相关文章:

  • base64魔改算法 | jsvmp日志分析并还原
  • 在 Dell PowerEdge T440 上通过 iDRAC9 安装 Proxmox VE
  • Flutter开发实战之网络请求与数据处理
  • bmp280的压力数据采集(i2c设备驱动+设备树编写)
  • ACO-OFDM 的**频带利用率**(单位:bit/s/Hz)计算公式
  • 建筑施工场景下漏检率↓76%!陌讯多模态融合算法在工程安全监控的落地实践
  • OpHReda精准预测酶最佳PH
  • 进制间的映射关系
  • 2025牛客暑期多校第4场——G
  • Polyhedral Approaches in Combinatorial Optimization组合优化中的多面体方法(下)
  • Java实现大根堆与小根堆详解
  • 每日面试题15:如何解决堆溢出?
  • 如何检查服务器数据盘是否挂载成功?
  • Android-三种持久化方式详解
  • 【硬件-笔试面试题】硬件/电子工程师,笔试面试题-32,(知识点:模数转换器,信噪比,计算公式,)
  • 深入理解C语言快速排序与自省排序(Introsort)
  • 【每天一个知识点】GAN(生成对抗网络,Generative Adversarial Network)
  • Compose笔记(三十八)--CompositionLocal
  • 安卓学习记录1——持续更新ing
  • React组件中的this指向问题
  • 三防平板支持DMR对讲有什么用?实现高效集群调度
  • 如何理解“测试场景”与“测试要点”的区别和联系?
  • Linux系统架构核心全景详解
  • 从0到1学Pandas(六):Pandas 与数据库交互
  • KiCad 与 CircuitMaker 使用方法分享:从零开始学电子设计
  • JavaWeb(苍穹外卖)--学习笔记11(Filter(过滤器) 和 Interceptor(拦截器))
  • Windows开发,制作开发软件安装程序(一)
  • MySQL的底层原理--InnoDB数据页结构
  • 关于GateWay网关
  • 基于HMM的词性标注方法详解(HMM+Viterbi,例题分析)