前言:在当今这个信息技术日新月异的时代,网络编程已成为连接世界、构建各类互联网应用不可或缺的一部分。而Linux,作为开源操作系统的典范,其强大的网络功能和灵活性,为开发者们提供了一个广阔而深入的实践平台。在众多网络编程技术中,套接字(Socket)编程无疑是核心与基石,它不仅支撑着Web服务、即时通讯、在线游戏等日常应用,还是实现分布式系统、云计算服务的关键技术之一。
在套接字编程的世界里,UDP(用户数据报协议)与TCP(传输控制协议)如同双生子,各自以其独特的优势占据着不同的应用场景。TCP,以其可靠的连接导向、顺序传输和错误校正机制,成为了追求数据传输完整性和稳定性的不二之选,如文件传输、远程登录等场景;而UDP,则以其无连接、快速传输和较小的开销,在实时性要求高、对丢包容忍度较大的场合,如视频流、在线游戏等中大放异彩。
本文旨在深入探讨Linux环境下,如何通过套接字编程技术,驾驭UDP与TCP这两种强大的网络传输协议,从零开始构建基础的网络通信能力。
让我们一同踏上这段探索之旅,揭开Linux网络编程的神秘面纱,领略UDP与TCP的魅力所在,共同构建更加智能、互联的世界!
概念:
端口号(port)是传输层协议的内容:
注意
:端口号和进程ID都可以唯一表示一个进程, 但是一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号 简单来说就是 “
数据是哪个发的, 最后要发给谁
”
TCP(Transmission Control Protocol 传输控制协议):
可靠传输
UDP(User Datagram Protocol 用户数据报协议):
不可靠传输
网络字节序(Network Byte Order),也称为网络字节顺序,是协议中规定好的一种数据表示格式。它用于在计算机网络中进行数据通信时,统一数据的字节顺序,确保数据在不同主机之间传输时能够被正确解释。
因为内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,为了定义出网络数据流的地址,我们引入了网络字节序。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
常见通用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是一个在C语言网络编程中使用的数据结构,主要用于表示套接字地址。它是一个通用的结构体,能够用于表示不同类型的套接字地址,如IPv4、IPv6等
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;
};
我们在进行套接字编程时,难免会多次用到一些转换,我们不妨将他们封装起来,让我们的代码变得不那么复杂,从而更容易获取我们想要的数据
#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_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;
};
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;
};
在之前的学习中,
head -1
我们经常使用,但是机会没机会了解PGID和SID
是啥意思,今天我们来正式认识一下
我们同时创建的sleep 10000,sleep 20000,sleep 30000,会不会在同一个进程组里面呢?我们可以查询一下
移动进程组的指令:
查看任务:
jobs
将任务放置到前台:fg task_number
将任务放置到后台:ctrl + Z
再dg task_number
守护进程:
守护进程是在后台运行的、不受任何终端控制的进程。它们通常用于执行系统级的任务或服务,如系统监控、网络通信、文件服务等。
TCP协议的客户端/服务器程序的一般流程:
建立连接的过程:
这个建立连接的过程, 通常称为 三次握手
断开连接的过程:
这个断开连接的过程, 通常称为 四次挥手
udp是面向用户数据包,而tcp面向字节流 — 数据和数据是有边界的
在探索Linux网络编程的浩瀚领域中,UDP与TCP作为两大核心协议,不仅构建了互联网通信的基石,也成为了每一位网络开发者必须掌握的利器。通过这段旅程,我们一同见证了从基础概念到实践应用的华丽蜕变,从最初的套接字创建、绑定、监听,到数据的发送与接收,每一步都充满了挑战与收获。
在结束这篇文章之际,愿每一位读者都能在网络编程的世界里找到自己的位置,用代码编织梦想,用技术照亮未来。让我们携手前行,在Linux网络编程的广阔天地中,共同书写属于我们的辉煌篇章!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行! 谢谢大家支持本篇到这里就结束了,祝大家天天开心!