文章目录
- Linux网络
- 1. UDP
- 1.1 UDP接口使用
- 1.1 UDP程序实例
- 2. TCP
- 2.1 TCP接口使用
- 2.2 TCP程序实例
Linux网络
1. UDP
在使用我们的UDP和TCP函数的时候,我们需要理解一些预备的知识:
源 IP 地址和目的 IP 地址:
在网络通信中,IP 数据包头部的源 IP 地址和目的 IP 地址起着至关重要的作用。源 IP 地址指明了数据包的发送方所在的网络位置,就像寄信时的发信人地址;目的 IP 地址则指明了数据包的接收方所在的网络位置,如同收信人的地址。
例如,当您在网上浏览网页时,您的设备(如电脑或手机)发送的请求数据包中就包含了您设备的源 IP 地址,而您所请求的网站服务器的 IP 地址则作为目的 IP 地址。
但仅有 IP 地址并不足以完成完整的通信。以发送 QQ 消息为例,虽然 IP 地址能将消息送达对方的机器,但机器上可能运行着多个程序,还需要其他标识来确定这个数据应由哪个程序来解析处理,这就引入了端口号的概念。
端口号:
端口号是传输层协议的重要组成部分,它是一个 2 字节 16 位的整数。端口号的作用在于标识一个进程,告诉操作系统应该将接收到的数据交给哪一个进程来处理。
比如说,您的电脑上同时运行着浏览器和即时通讯软件,当网络数据到达时,端口号就像一个“指示牌”,告诉操作系统把数据准确地传递给对应的程序。
就像一个电话号码,IP 地址能让数据找到正确的主机,而端口号则能让数据找到主机上正确的进程。
IP 地址和端口号的组合能够唯一标识网络上某一台主机的某一个进程。
端口号和进程 ID(pid):
在系统编程中,pid 用于唯一标识一个进程。而端口号在某种程度上也类似于 pid 对进程的标识作用。
然而,它们之间也存在一些区别。pid 是操作系统内部用于管理进程的标识符,而端口号则主要用于网络通信中标识进程。
例如,在一个服务器系统中,操作系统通过 pid 来管理进程的资源分配和调度,而当网络数据到达时,服务器通过端口号来确定将数据传递给哪个网络服务进程。
源端口号和目的端口号:
在传输层协议(如 TCP 和 UDP)的数据段中,源端口号和目的端口号分别描述了“数据是谁发的,要发给谁”。
比如,您通过浏览器访问一个网站,您的浏览器使用一个随机分配的源端口号(通常是大于 1024 的临时端口)向网站服务器的特定目的端口号(如 HTTP 协议通常使用 80 端口)发送请求,服务器接收到请求后,根据源端口号和目的端口号进行回应,从而完成一次完整的网络通信过程。
1.1 UDP接口使用
// 创建 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 套接字:
// 创建 UDP 套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
“socket(AF_INET, SOCK_DGRAM, 0)”这个函数调用是用来创建一个套接字。
“AF_INET”意思是使用 IPv4 地址格式。
“SOCK_DGRAM”表示创建的UDP数据报套接字,数据以独立的包发送,可能无序、丢失或重复。
“0”让系统自动选择适合前面设置的默认传输协议,这里就是 UDP 协议。
函数成功会返回一个套接字描述符,用于后续操作;失败则返回 -1 。
绑定端口号:
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 填充服务器地址信息
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字到指定端口
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
我们首先定义了两个 struct sockaddr_in 类型的结构体 servaddr(服务器地址)和 cliaddr(客户端地址)。
然后使用 memset 函数将这两个结构体的内存空间初始化为 0,以清除可能存在的垃圾数据。
接下来为服务器地址结构体 servaddr 进行填充:
servaddr.sin_family 设为 AF_INET,表示使用 IPv4 地址格式。
servaddr.sin_addr.s_addr 设为 INADDR_ANY,表示服务器可以接收来自任何本地 IPv4 地址的连接请求。
servaddr.sin_port 通过 htons 函数将指定的端口号(比如前面定义的 PORT)转换为网络字节序并进行设置。
最后,使用 bind 函数将创建的套接字 sockfd 与填充好的服务器地址 servaddr 进行绑定。如果绑定失败,会打印错误信息并退出程序。
网络字节序:
网络字节序是网络数据流的规定格式,采用大端字节序(低地址高字节)。
TCP/IP 协议要求网络数据按这种格式传输。发送主机按内存地址从低到高发送数据,接收主机也按内存地址从低到高保存数据。
不管主机是大端还是小端字节序,都要按网络字节序发送/接收数据。小端主机需转换数据,大端主机可直接发送。
为让网络程序在不同主机都能正常运行,可调用库函数进行网络字节序和主机字节序转换,如 htons、htonl、ntohs、ntohl 。
sockaddr结构:
Socket API 是适用于多种底层网络协议的抽象编程接口,如 IPv4、IPv6 及 UNIX Domain Socket 。IPv4 地址用 sockaddr_in 结构体表示,其包含地址类型、端口号和 IP 地址。
IPv4、IPv6 地址类型分别定义为常数 AF_INET 、 AF_INET6 。Socket API 都能用 struct sockaddr * 类型表示,使用时需强制转化为 sockaddr_in ,好处是使程序具有通用性,能接收各种类型的 sockaddr 结构体指针作为参数。
sockaddr_in 结构:
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in,这个结构里主要有三部分信息:地址类型,端口号,IP地址。
in_addr结构:
in_addr用来表示一个IPv4的IP地址,其实就是一个32位的整数。
1.1 UDP程序实例
UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer{
public:
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false)
{}
void Init()
{
// 1. 创建udp socket
// 2. Udp 的socket是全双工的,允许被同时读写的
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
if(sockfd_ < 0)
{
lg(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", sockfd_);
// 2. bind socket
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
// local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}
void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport)
{
auto iter = online_user_.find(clientip);
if(iter == online_user_.end())
{
online_user_.insert({clientip, client});
std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
}
}
void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport)
{
for(const auto &user : online_user_)
{
std::string message = "[";
message += clientip;
message += ":";
message += std::to_string(clientport);
message += "]# ";
message += info;
socklen_t len = sizeof(user.second);
sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
}
}
void Run() // 对代码进行分层
{
isrunning_ = true;
char inbuffer[size];
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
std::string clientip = inet_ntoa(client.sin_addr);
CheckUser(client, clientip, clientport);
std::string info = inbuffer;
Broadcast(info,clientip, clientport);
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 任意地址bind 0
uint16_t port_; // 表明服务器进程的端口号
bool isrunning_;
std::unordered_map<std::string, struct sockaddr_in> online_user_;
};
UdpClient.cc
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
std::string serverip;
};
void *recv_message(void *args)
{
// OpenTerminal();
ThreadData *td = static_cast<ThreadData *>(args);
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
cerr << buffer << endl;
}
}
}
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
string message;
socklen_t len = sizeof(td->server);
std::string welcome = td->serverip;
welcome += " comming...";
sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
while (true)
{
cout << "Please Enter@ ";
getline(cin, message);
// std::cout << message << std::endl;
// 1. 数据 2. 给谁发
sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
}
}
// 多线程
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct ThreadData td;
bzero(&td.server, sizeof(td.server));
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport); //?
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
td.serverip = serverip;
pthread_t recvr, sender;
pthread_create(&recvr, nullptr, recv_message, &td);
pthread_create(&sender, nullptr, send_message, &td);
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
close(td.sockfd);
return 0;
}
2. TCP
TCP | UDP |
---|---|
可靠传输 | 不可靠传输 |
有连接 | 无连接 |
字节流 | 数据报 |
2.1 TCP接口使用
// 创建 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);
// 开始监听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);
创建TCP套接字:
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket creation failed");
return -1;
}
socket(AF_INET, SOCK_STREAM, 0) 这个函数来做创建套接字。
AF_INET 是 IPv4 地址族。
SOCK_STREAM 表示要创建 TCP 套接字。
最后的 0 让系统选合适协议,这里就是 TCP 协议。
要是创建失败(server_fd 为 0 ),就输出错误信息,程序返回 -1 结束。
绑定套接字:
struct sockaddr_in address;
int addrlen = sizeof(address);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
return -1;
}
我们先定义了一个 sockaddr_in 结构体 address 并获取其大小。然后为 address 结构体的成员赋值:
sin_family 设为 AF_INET,表示使用 IPv4 地址格式。
sin_addr.s_addr 设为 INADDR_ANY,表示服务器可以接收来自任何本地 IPv4 地址的连接请求。
sin_port 通过 htons 函数将指定的端口号(比如前面定义的 PORT)转换为网络字节序并进行设置。
接下来使用 bind 函数将创建的套接字 server_fd 与填充好的 address 结构体进行绑定。如果绑定失败,会输出错误信息并返回 -1 结束程序。
监听套接字:
// 监听
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
return -1;
}
我们使用 listen 函数让服务器套接字 server_fd 进入监听状态。
参数 3 表示等待连接队列的最大长度。
如果 listen 函数执行失败(返回值小于 0 ),就会输出错误信息 “Listen failed” ,然后程序返回 -1 结束运行。
接收连接:
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("Accept failed");
return -1;
}
我们通过 accept 函数来接受客户端的连接请求。
如果成功,会返回一个新的套接字描述符 new_socket ,用于与客户端进行后续的通信。
同时,会将客户端的地址信息填充到 address 结构体中,addrlen 则用于记录客户端地址的长度。
如果 accept 函数执行失败(返回值小于 0 ),就会输出错误信息 “Accept failed” ,然后程序返回 -1 结束。
接收数据:
// 接收数据
int received = recv(new_socket, buffer, sizeof(buffer), 0);
if (received < 0) {
perror("Receive failed");
return -1;
}
我们使用 recv 函数从与客户端连接的新套接字 new_socket 中接收数据,并将接收到的数据存储到 buffer 数组中。
received 变量用于记录实际接收到的数据字节数。
如果接收数据失败(received 的值小于 0 ),就会输出错误信息 “Receive failed” ,然后程序返回 -1 结束。
发送数据:
// 发送数据
char *response = "Hello client!";
send(new_socket, response, strlen(response), 0);
我们使用 send 函数通过新套接字 new_socket 向客户端发送这个数据。
strlen(response) 用于获取响应字符串的长度。
最后一个参数 0 表示默认的发送选项。
关闭连接:
close(new_socket);
close(server_fd);
close(new_socket) 关闭与客户端通信的套接字 new_socket 。
close(server_fd) 关闭服务器端创建的监听套接字 server_fd 。
关闭套接字可以释放相关的资源,并结束与客户端的连接和服务器的监听状态。
2.2 TCP程序实例
TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError,
};
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
{}
public:
int sockfd;
std::string clientip;
uint16_t clientport;
TcpServer *tsvr;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip)
{
}
void InitServer()
{
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, listensock_: %d", listensock_);
int opt = 1;
setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
// local.sin_addr.s_addr = INADDR_ANY;
if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, listensock_: %d", listensock_);
// Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
if (listen(listensock_, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "listen socket success, listensock_: %d", listensock_);
}
// static void *Routine(void *args)
// {
// pthread_detach(pthread_self());
// ThreadData *td = static_cast<ThreadData *>(args);
// td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//???
// delete td;
// return nullptr;
// }
void Start()
{
Daemon();
ThreadPool<Task>::GetInstance()->Start();
// for fork();
// signal(SIGCHLD, SIG_IGN);
lg(Info, "tcpServer is running....");
for (;;)
{
// 1. 获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// std::cout << "hello world" << std::endl;
// version 1 -- 单进程版
// Service(sockfd, clientip, clientport);
// close(sockfd);
// version 2 -- 多进程版
// pid_t id = fork();
// if(id == 0)
// {
// // child
// close(listensock_);
// if(fork() > 0) exit(0);
// Service(sockfd, clientip, clientport); //孙子进程, system 领养
// close(sockfd);
// exit(0);
// }
// close(sockfd);
// // father
// pid_t rid = waitpid(id, nullptr, 0);
// (void)rid;
// version 3 -- 多线程版本
// ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, Routine, td);
// version 4 --- 线程池版本
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
// void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
// {
// // 测试代码
// char buffer[4096];
// while (true)
// {
// ssize_t n = read(sockfd, buffer, sizeof(buffer));
// if (n > 0)
// {
// buffer[n] = 0;
// std::cout << "client say# " << buffer << std::endl;
// std::string echo_string = "tcpserver echo# ";
// echo_string += buffer;
// write(sockfd, echo_string.c_str(), echo_string.size());
// }
// else if (n == 0)
// {
// lg(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
// break;
// }
// else
// {
// lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
// break;
// }
// }
// }
~TcpServer() {}
private:
int listensock_;
uint16_t port_;
std::string ip_;
};
TcpClient.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
int cnt = 5;
int isreconnect = false;
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 1;
}
do
{
// tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口
// 客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
isreconnect = true;
cnt--;
std::cerr << "connect error..., reconnect: " << cnt << std::endl;
sleep(2);
}
else
{
break;
}
} while (cnt && isreconnect);
if (cnt == 0)
{
std::cerr << "user offline..." << std::endl;
break;
}
// while (true)
// {
std::string message;
std::cout << "Please Enter# ";
std::getline(std::cin, message);
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
std::cerr << "write error..." << std::endl;
// break;
}
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
else{
// break;
}
// }
close(sockfd);
}
return 0;
}