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

【linux网络】网络编程全流程详解:从套接字基础到 UDP/TCP 通信实战

网络套接字编程

共识

我们知道通过目标主机的IP(公网IP)就可以将数据进行发送到目标主机进行网络通信,但是我们思考将数据进行发送到目标主机这就是我们真正的目的吗??

其实并不是,我们将数据进行发送到目标主机只是我们的第一步,我们的目的是进行客户端之间的通信,并不是进行机器间的通信,进行客户端之间的通信本质上就是特定进程之间的通信,信号在主机之间的转发仅仅是手段。

端口号

数据经过传输层进行数据的解包后,交给用户层,我们怎么知道是交给哪个进程进行处理,这就引入的端口号。通过端口号进行记录特定的进程,就可以将处理后的信息交给特定进程进行处理,这就完成了客户端之间的通信。

套接字

套接字是应用层和网络技术找之间的接口,在网络通信中,套接字在操作系统层面是网络连接的一个抽象表示。它通过源 IP 地址(SRC_IP)源端口(SRC_PORT)目标 IP 地址(DST_IP)目标端口(DST_PORT) 来定义一个唯一的网络通信连接。

UDP协议

  • 传输层的协议
  • 无连接(在进行写代码的时候无需进行考虑创建链接)
  • 不可靠传输
  • 面向数据报

TCP协议

  • 传输层的协议
  • 有链接
  • 可靠传输
  • 面向字节流

网络字节序列

数据在进行存储的模式分为大端存储(高权值位放到高地址处)和小端存储,两台主机在通过网络进行通信时,假如主机A进行将数据进行大端存储,而主机B在进行读取数据的时候按照小端的方式进行读取数据,就容易出现数据内容混乱问题。

我们考虑如果我们通过在数据中进行标记出数据是以什么方式进行存储的,这样能否解决问题呢??答案是否定的,我们通过增加标记位的方式进行说明数据进行存储的方式,那么这个标记为应该在数据的大端还是小端呢??假如数据存储的标记位在大端,主机B通过小端开始进行读取数据的时候还是无法进行正确读取数据。

这里采取最直接简短粗暴的方法进行解决,直接进行规定:数据在网络中进行通信的方式一律采取大端存取的方式,这种方式就称为网络字节序

在后续我们如果需要用到大小端的转化直接通过下面接口进行

h的含义是主机,to表示转化,n的含义是网络,l表示4字节的数据,s表示2字节的数据。

以htonl为例,无论我们的主机是大端存储还是小端存储,通过htonl都是进行转化成网络进行数据存储的方式(也就是大端存储)。

套接字的通用接口

  • // 创建 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);

常见的套接字

域间套接字,用于本主机内进行通信

原始套接字:主要用于编写工具,可以直接绕过传输层和网络层直接到达底层。

网络套接字

由于套接字的种类不同,理论上应该是三套接口,但是Linux不想进行设计过多的接口,本着将所有接口进行统一的原则

从上面的套接字通用接口我们就可以看出,所有的接口中的参数的指针都是struct sockaddr* ,那我我们进行使用域间套接字和网络套接字的接口是如何进行分辨的呢?

其实是通过进行读取前两个字节,根据读取的类型不同从而进行判断。

简单的UDP网络程序

将UDPsocket接进行封装成通用服务器

初始化服务器

1、创建套接字

套接字是操作系统进行提供的一种编程接口,用于进行不同主机之间的网络通信。0

函数原型及头文件

参数解析

domain

用于指定套接字的地址族(即协议族),常见值如下

  • AF_INET:IPv4协议。
  • AF_INET6:IPv6协议。
  • AF_UNIX 或 AF_LOCAL:本地套接字,进程间通信。
  • AF_PACKET:直接访问网络设备,通常用于底层的网络通信。

type

用于指定套接字的类型,进行定义通信的方式,常见值如下

  • SOCK_STREAM:流式套接字,通常用于面向连接的协议,如 TCP。
  • SOCK_DGRAM:数据报套接字,通常用于无连接的协议,如 UDP。
  • SOCK_RAW:原始套接字,用于直接操作网络层。

protocal

用于指定套接字具体的协议,一般设置成0,有操作系统根据domain和type自己进行筛选出合适的协议,如果想要自己进行设置,可以进行显示设置

返回值说明

如果创建套接字成功,进行返回一个类似于文件操作符(即套接字操作符)的数字,该返回值大于等于0;如果创建失败,返回-1。

2、进行bind绑定

IP地址标定主机的唯一性,port端口标定进程的唯一性,将IP地址和port在内核中和我们的进程进行强关联

函数原型及头文件

参数说明

socked

套接字操作符

addr

原始套接字的指针,根据传入的参数进行区分是域间套接字还是网络套接字

addrlen

addr指向结构体的大小

返回值

成功时返回0;失败时返回-1

启动服务器

服务器一旦进行其中没有什么特定情况,一般是不进行退出的,进行启动服务器首先要进行考虑的就是死循环问题。进行启动服务器,服务器要进行的工作可以进行总结成以下几点

  • 接收客户端进行发来的数据
  • 进行分析和处理数据
  • 最后将分析和处理数据的结果按照要求进行返回
1、接收客户端发来的数据

recvfrom(receive from)

参数解释

sockfd:

已经进行打开的套接字描述符

buf:

这是一个指向缓冲区的指针,用于存储接收到的数据。需要在调用 recvfrom() 前分配好该缓冲区,并且缓冲区的大小要足够容纳接收的数据。

len:

这是缓冲区的大小,指定 recvfrom() 能够接收的最大字节数。这个值应该是缓冲区的大小。

flags:

该参数可以控制接收的行为,常用的标志包括:其他一些控制标志,详细信息可以参考 Linux 文档。MSG_WAITALL: 等待完整的数据包被接收(可能会阻塞)。MSG_PEEK: 允许查看数据而不移除它。

src_addr:

这是一个指向 struct sockaddr 类型的指针,用于存储发送方的地址信息。调用者可以通过它来获取发送者的 IP 地址、端口等信息。如果不需要获取发送者的地址,可以将此参数设置为 NULL

addrlen(输入输出型参数)

这是一个指向 socklen_t 类型的指针,用来存储地址结构的长度。

返回值

返回值是实际接收到的字节数,类型为 ssize_t。如果成功接收到数据,它返回接收到的字节数。如果没有数据可读(比如对方关闭了连接),则返回 0。

如果发生错误,返回 -1,并且设置 errno 变量指示具体的错误原因。常见的错误包括:

EAGAIN 或 EWOULDBLOCK: 非阻塞套接字且没有数据可接收。

EINVAL: 提供的地址结构不正确。

EBADFsockfd 无效。

2、进行分析和处理数据
3、将分析处理后的数据进行按照要求进行返回

sendto

参数解释

sockfd

套接字描述符

buf

一个指向要发送数据缓冲区的指针。该缓冲区存储着需要发送的数据。

len

要发送的数据的长度,单位是字节。

flags

标志,通常设置为 0。这个参数可以控制发送行为,例如可以设置 MSG_DONTWAIT 来使发送操作非阻塞。

dest_addr

目标地址的信息,它是一个指向 struct sockaddr 类型的指针。在发送数据时, 需要知道目标主机的地址。 

addrlen

dest_addr 地址结构的大小

返回值

如果发送成功,返回发送的字节数(即发送的数据长度)。如果数据长度较小,可能会出现部分发送的情况。

如果出错,返回 -1,并设置 errno 来指示错误原因

UDP程序

函数原型

FILE *popen(const char *command, const char *type);

参数说明

  • command: 这是一个字符串,表示要执行的 shell 命令。例如 "ls -l" 或 "grep hello"

  • type: 这是一个字符串,指定管道的类型。可以是:

    • "r": 表示从命令的输出中读取数据(即命令的标准输出会被重定向到管道)。

    • "w": 表示向命令的输入中写入数据(即命令的标准输入会被重定向到管道)。

返回值

  • 成功时,popen 返回一个指向 FILE 结构的指针,该指针可以用于后续的 freadfgetsfwrite 等标准 I/O 函数。

  • 失败时,返回 NULL,并设置 errno 以指示错误原因。

popen这个函数相当于pipe+fork+exec*系列函数

通过创建一个管道来进行命令的输入与输出进行交互

UdpServer.hpp

#ifndef UDP_SERVER_HPP
#define UDP_SERVER_HPP
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include "log.hpp"
#include <cstdio>
#include <functional>#define SIZE 1024typedef std::function<void(int,std::string,uint16_t,std::string)> func_t;
class UdpServe
{public:UdpServe(const std::string ip, uint16_t port,func_t func): _ip(ip), _port(port),_func(func){}// 初始化服务器void InitServer(){// 1、创建套接字_sockid = socket(AF_INET, SOCK_DGRAM, 0);if (_sockid == -1){perror("socket");exit(1);}log("创建套接字成功.....");// 2、进行bind绑定struct sockaddr_in local;// 2.1 初始化localbzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());int n = bind(_sockid, (struct sockaddr *)&local, sizeof(local));if (n == -1){perror("bind");exit(2);}log("进行bind绑定成功.....");}// 启动服务器void StartServer(){log("启动服务器成功.....");for (;;){// 进行读取数据struct sockaddr_in peer;bzero(&peer, sizeof(peer));socklen_t len = sizeof(peer);char buffer[SIZE];memset(buffer, 0, sizeof(buffer));//std::cout<<"正在进行读取数据"<<std::endl;int m = recvfrom(_sockid, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);//std::cout<<"读取数据完成"<<std::endl;if (m == -1){perror("recvfrom");exit(3);}if (m > 0){buffer[m] = 0;// 输出收到的消息// 谁发的uint16_t cli_port = ntohs(peer.sin_port);std::string cli_ip = inet_ntoa(peer.sin_addr);printf("[%s,%d]#%s\n", cli_ip.c_str(), cli_port, buffer);// 通过回调方法对接收到的消息进行处理std::string message=buffer;_func(_sockid,cli_ip,cli_port,message);}// 进行分析和处理数据// 进行将数据处理的结果进行写回}}~UdpServe(){// do nothing}private:std::string _ip;uint16_t _port;int _sockid;func_t _func;
};
#endif

UdpServer.cpp

#include"TCP_client.hpp"
#include<memory>int main(int args,char* argv[])
{if(args!=3){std::cout<<"please enter it is followint format "<<std::endl;std::cout<<"standerd format: ./TCP_client ip port"<<std::endl;exit(1);}std::string ip=argv[1];uint16_t port=atoi(argv[2]);std::unique_ptr<TcpClient> tc(new TcpClient(ip,port));//初始化服务端tc->InitClient();//启动服务端tc->StartClient();return 0;
}

UdpClient.cpp

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<strings.h>
#include<arpa/inet.h>int main(int args,char* argv[])
{if(args!=3){std::cout<<"please enter it in following format"<<std::endl;std::cout<<"standard format“./UDP_client IP port”"<<std::endl;exit(1);}//进行创建套接字int sockid=socket(AF_INET,SOCK_DGRAM,0);if(sockid<0){perror("sockid");exit(2);}//需要进行进行bind,OS进行随机选择std::string message;struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_addr.s_addr=inet_addr(argv[1]);server.sin_port=htons(atoi(argv[2]));while(true){std::cout<<"请进行输入你的信息:"<<std::endl;std::getline(std::cin,message);//进行发送数据int n=sendto(sockid,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));//进行接收信息struct sockaddr_in temp;socklen_t len=sizeof(temp);char buffer[1024];int m=recvfrom(sockid,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m>0){buffer[1024]=0;std::cout<<"server echo#"<<buffer<<std::endl;}}return 0;
}

 

简单的TCP网络程序

TcpServer.hpp

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include "log.hpp"
static int gbacklog = 5;
int sockid = -1;
#define SIZE 1024
class TcpServer
{
public:TcpServer(uint16_t port): _listen_sockid(-1), _port(port){}void InitServer(){// 进行创建套接字_listen_sockid = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockid < 0){log("create socket fales", 4);exit(1);}log("create socket sucesses", 1);std::cout << "listen_sockid:" << _listen_sockid << std::endl;// 进行bind绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(_port);socklen_t len = sizeof(local);int n = bind(_listen_sockid, (struct sockaddr *)&local, len);if (bind < 0){log("bind fales", 4);exit(2);}log("bind sucesses", 1);// 设置socket为监听状态                  //哪些客户端申请进行访问“我”(服务器)int m = listen(_listen_sockid, gbacklog);if (m < 0){log("listen false", 4);exit(3);}log("listen sucesses", 1);}void StartServer(){// 进行获取链接struct sockaddr_in client;memset(&client, 0, sizeof(client));socklen_t len = sizeof(client);sockid = accept(_listen_sockid, (struct sockaddr *)&client, &len);if (sockid < 0){log("accept false", 4);exit(4);}log("accept sucesses", 1);// 进行通信ServerIO();}void ServerIO(){// echo服务器while (true){// 进行读取数据char buffer[SIZE];ssize_t n=read(sockid, buffer, sizeof(buffer) - 1);if(n<0){log("read false",3);exit(5);}buffer[n]=0;std::cout<<"Received a message : "<<buffer<<std::endl;// 将处理过的数据进行发送回去std::string message;message=buffer;message+="   |   server echo";write(sockid,message.c_str(),message.size());}}~TcpServer(){if(sockid>0){close(sockid);}}private:uint16_t _port;int _listen_sockid;
};

 TcpServer.cpp

#pragma once
#include <iostream>
#include <memory>
#include "UDP_server.hpp"
#include <string.h>
#include <unordered_map>
#include <fstream>// demo1
// echo服务器
void echo(int sockid, std::string cli_ip, uint16_t cli_port, std::string buff)
{char buffers[1024];memcpy(buffers, buff.c_str(), buff.size());struct sockaddr_in peers;socklen_t len = sizeof(peers);bzero(&peers, sizeof(peers));peers.sin_port = htons(cli_port);peers.sin_family = AF_INET;peers.sin_addr.s_addr = inet_addr(cli_ip.c_str());int k = sendto(sockid, buffers, sizeof(buffers), 0, (struct sockaddr *)&peers, len);if (k == -1){perror("sendto");exit(4);}
}// demo2
// 翻译服务器
std::unordered_map<std::string, std::string> dirt;
// 用于分割字符串
bool translateRun(std::string &line, std::string &key, std::string &value, const std::string &temp)
{auto pos = line.find(temp); // 查找“ :”if (pos == std::string::npos){return false;}key = line.substr(0, pos);value = line.substr(pos + temp.size());return true;
}
void translateInit()
{// 通过二进制进行读取文件std::ifstream in("./dictText", std::ios::binary);if (!in.is_open()){std::cerr << "open file error" << std::endl;exit(1);}// 进行读取流输入缓冲区中的数据std::string line;std::string key, val;while (getline(in, line)){// std::cout<<line<<std::endl;// 进行在词库中进行寻找判断if (translateRun(line, key, val, ":")){dirt.insert(make_pair(key, val));}}in.close();
}void test()
{for (auto &e : dirt){std::cout << e.first << " # " << e.second << std::endl;}
}void translation(int sockid, std::string cli_ip, uint16_t cli_port, std::string buff)
{translateInit();// test();// 利用哈希表进行查找auto it = dirt.find(buff);std::string word;if (it == dirt.end()){word = "unkown";}// 词库中存在这个单词进行翻译else{word = it->second;}// 进行想客户端将翻译后的消息进行返回struct sockaddr_in client;client.sin_addr.s_addr = inet_addr(cli_ip.c_str());client.sin_family = AF_INET;client.sin_port = htons(cli_port);socklen_t len = sizeof(client);int n = sendto(sockid, word.c_str(), word.size(), 0, (struct sockaddr *)&client, len);if (n < 0){perror("server sendto");exit(1);}
}// demo3
// 远程shell
void remoteShell(int sockid, std::string cli_ip, uint16_t cli_port, std::string buff)
{std::string response;FILE *fp = popen(buff.c_str(), "r");if (fp == nullptr){response = buff + "<- check the enter";}else{char line[1024];while (fgets(line, sizeof(line), fp)){response += line;}}pclose(fp);// 将结果进行返回struct sockaddr_in client;client.sin_addr.s_addr = inet_addr(cli_ip.c_str());client.sin_family = AF_INET;client.sin_port = htons(cli_port);socklen_t len = sizeof(client);int n = sendto(sockid, response.c_str(), response.size(), 0, (struct sockaddr *)&client, len);if (n < 0){perror("server sendto");exit(1);}
}int main(int args, char *argv[])
{// 补:在进行通过命令行进行换取参数时,需要指定格式: ./Udp_server 0.0.0.0 8888// 如果用户格式不正确进行打印提醒if (args != 3){std::cout << "Please enter it in the following format" << std::endl;std::cout << "standard format:“./Udp_server ip port ”" << std::endl;exit(1);}// 1、通过智能指针进行创建UDP服务端对象std::string ip = argv[1];uint16_t port = atoi(argv[2]);// demo1// std::unique_ptr<UdpServe> us(new UdpServe(ip, port, echo)); // 创建出服务端需要进行传参--命令行参数进行// demo2// std::unique_ptr<UdpServe> us(new UdpServe(ip, port, translation));// demo3std::unique_ptr<UdpServe> us(new UdpServe(ip, port, remoteShell));// 2.初始化服务器us->InitServer();// 3.进行启动服务器us->StartServer();return 0;
}

 TcpClient.hpp

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <strings.h>
#include <arpa/inet.h>class Udp_Client
{
public:Udp_Client(const std::string &ip, const uint16_t port): _ip(ip), _port(port),_sockid(-1){}// 进行初始化客户端void InitClient(){// 进行创建套接字_sockid = socket(AF_INET, SOCK_DGRAM, 0);if (_sockid < 0){perror("sockid");exit(2);}}//进行气功客户端void StartClient(){std::string message;// 需要进行进行bind,OS进行随机选择struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(_ip.c_str());server.sin_port = htons(_port);while(true){std::cout<<"请进行输入你的信息:"<<std::endl;std::getline(std::cin,message);if(message.size()==0){std::cout<<"you enter data is empty,please cononical enter"<<std::endl;return;}//进行发送数据int n=sendto(_sockid,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));//进行接收信息struct sockaddr_in temp;socklen_t len=sizeof(temp);char buffer[1024]="";int m=recvfrom(_sockid,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m>0){buffer[1024]=0;std::cout<<"server echo #  "<<buffer<<std::endl;}}}private:uint16_t _port;std::string _ip;int _sockid;
};

 TcpClient.cpp

#include"UDP_client.hpp"
#include<memory>
int main(int args,char* argv[])
{if(args!=3){std::cout<<"please enter it in following format"<<std::endl;std::cout<<"standard format“./UDP_client IP port”"<<std::endl;exit(1);}std::string ip=argv[1];uint16_t port=atoi(argv[2]);std::unique_ptr<Udp_Client> uc(new Udp_Client(ip,port));//进行初始化客户端uc->InitClient();//进行运行客户端uc->StartClient();return 0;
}

TCP进行通讯的流程

几点声明

  • 我们在使用TCP套接字进行网络通讯时本质是通过两个操作系统之间在进行通信,操作系统是通过三次握手和四次挥手进行的
  • 操作系统的接口只是只是在发起建立链接的请求
  • 建立链接的实质是操作系统在对信息进行先描述再组织

服务器的初始化

  1. 创建套接字
  2. 进行bind绑定
  3. 调用listen ,声明socket创建成功的返回值是服务器的文件描述符
  4. 进行accept阻塞等待来自客户端的链接申请

建立链接(三次握手)

  1. 客户端进行创建套接字
  2. 通过connect进行申请链接
  3. connect会发出SYN段并阻塞等待服务器应答; (第一次)
  4. 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  5. 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

断开链接(四次挥手)

  1. 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  2. 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  3. read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)
  4. 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
http://www.dtcms.com/a/269981.html

相关文章:

  • 【Java安全】RMI基础
  • go go go 出发咯 - go web开发入门系列(二) Gin 框架实战指南
  • WiFi协议学习笔记
  • 点云的无监督语义分割方法
  • 寻找两个正序数组的中位数(C++)
  • 成都算力租赁新趋势:H20 八卡服务器如何重塑 AI 产业格局?
  • 基于 Rust 的Actix Web 框架的应用与优化实例
  • C++ 选择排序、冒泡排序、插入排序
  • mac安装docker
  • APISEC安全平台
  • 嵌入式学习笔记-MCU阶段-DAY01
  • WPF之命令
  • 使用elasticdump高效备份与恢复Elasticsearch数据
  • WebSocket详细教程 - SpringBoot实战指南
  • EPLAN 电气制图(四):EPLAN 总电源电路设计知识详解
  • mit6.5840-lab3-3D-SnapShot-25Summer
  • 常见前端开发问题的解决办法
  • 深度学习——神经网络1
  • JK触发器Multisim电路仿真——硬件工程师笔记
  • HMI安全设计规范:ISO 26262合规的功能安全实现路径
  • python2.7/lib-dynload/_ssl.so: undefined symbol: sk_pop_free
  • 查询依赖冲突工具maven Helper
  • 常见的网络攻击方式及防御措施
  • 人工智能与人工智障———仙盟创梦IDE
  • Go HTTP 调用(上)
  • LeetCode 1248.统计优美子数组
  • cocos2dx3.x项目升级到xcode15以上的iconv与duplicate symbols报错问题
  • 云原生时代的日志管理:ELK、Loki、Fluentd 如何选型?
  • C++11 算法详解:std::copy_if 与 std::copy_n
  • UVC(USB Video Class,USB 视频类)协议