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()
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; // 设置回调处理
};