【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
: 提供的地址结构不正确。
EBADF
:sockfd
无效。
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
结构的指针,该指针可以用于后续的fread
、fgets
、fwrite
等标准 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套接字进行网络通讯时本质是通过两个操作系统之间在进行通信,操作系统是通过三次握手和四次挥手进行的
- 操作系统的接口只是只是在发起建立链接的请求
- 建立链接的实质是操作系统在对信息进行先描述再组织
服务器的初始化
- 创建套接字
- 进行bind绑定
- 调用listen ,声明socket创建成功的返回值是服务器的文件描述符
- 进行accept阻塞等待来自客户端的链接申请
建立链接(三次握手)
- 客户端进行创建套接字
- 通过connect进行申请链接
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
断开链接(四次挥手)
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)