Linux网络

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)

每个数据包是独立的,有明确边界。sendto 和 recvfrom 每次收发一个完整的数据包。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()

代码语言:javascript复制int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);

recv(client_fd, buffer, sizeof(buffer), 0);UDP 服务端:直接 recvfrom()

代码语言:javascript复制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:

代码语言:javascript复制#pragma once

#include

#include

#include

#include

#include

#include

#include

#include

#include

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

代码语言:javascript复制#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:字典翻译类!!!

代码语言:javascript复制#pragma once

#include

#include

#include

#include

#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 _dict;

};dictionary.txt:

代码语言:javascript复制apple: 苹果

banana: 香蕉

cat: 猫

dog: 狗

book: 书

pen: 笔

happy: 快乐的

sad: 悲伤的

hello:

: 你好

run: 跑

jump: 跳

teacher: 老师

student: 学生

car: 汽车

bus: 公交车

love: 爱

hate: 恨

hello: 你好

goodbye: 再见

summer: 夏天

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

makefile:

代码语言:javascript复制.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 tcpserverTcpClient.cc:客户端

代码语言:javascript复制#include

#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:服务端

代码语言:javascript复制#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 tsvr = std::make_unique(port,

//std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));

// std::unique_ptr tsvr = std::make_unique(port, [&cmd](const std::string &command, InetAddr &addr)

// { return cmd.Execute(command, addr); });

;

std::unique_ptr tsvr = std::make_unique(port, [&d](const std::string &word, InetAddr &addr){

return d.Translate(word, addr);

});

tsvr->Init();

tsvr->Run();

return 0;

}TcpServer.hpp:

代码语言:javascript复制#pragma once

#include "Common.hpp"

#include "Log.hpp"

#include "InetAddr.hpp"

#include "ThreadPool.hpp"

#include

#include

#include

// 服务器往往是禁止拷贝的

using namespace LogModule;

using namespace ThreadPoolModule;

// using task_t = std::function;

using func_t = std::function;

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(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::GetInstance()->Enqueue([this, sockfd, &addr](){

// this->Service(sockfd, addr);

// });

}

_isrunning = false;

}

~TcpServer()

{

}

private:

uint16_t _port;

int _listensockfd; // 监听socket

bool _isrunning;

func_t _func; // 设置回调处理

};