【Linux网络编程】网络编程套接字(1)
目录
- 【Linux网络编程】网络编程套接字(1)
- 源IP地址和目的IP地址
- 端口号
- 端口号和进程ID的关系
- 网络通信
- TCP协议
- UDP协议
- 网络字节序
- socket编程接口
- 简单的UDP网络程序
作者:爱写代码的刚子
时间:2024.1.29
前言:先提前写网络编程的博客,管道以及多线程的博客之后补上。
源IP地址和目的IP地址
IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址
- 源IP地址(Source IP Address):
- 源IP地址是发送数据包的设备(或主机)的IP地址。
- 在一个网络通信过程中,源IP地址标识了消息的来源。
- 在IPv4中,源IP地址通常以四个十进制数字的形式表示,如 “192.168.0.1”。
- 在IPv6中,源IP地址以一种更长的形式表示,如 “2001:0db8:85a3:0000:0000:8a2e:0370:7334”。
- 目的IP地址(Destination IP Address):
- 目的IP地址是接收数据包的设备(或主机)的IP地址。
- 在一个网络通信过程中,目的IP地址标识了消息的目标。
- 同样,目的IP地址可以以IPv4或IPv6的形式表示。
端口号
端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用
端口号和进程ID的关系
- 当网络服务启动时,它会监听一个特定的端口号,等待客户端连接。
- 当客户端尝试连接到服务时,客户端和服务之间的通信将使用该端口号进行标识。
- 当连接建立后,服务的进程ID与该连接相关联。这使得操作系统可以跟踪网络连接与哪个进程相关。
【问题】:我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程。那么这两者之间是怎样的关系?
-
网络模块和进程管理模块进行解耦合。进程pid在技术上是可以标定当前主机上某一个唯一的进程,但是实际上不会用进程pid做,进程pid属于进程管理范畴,而端口号属于网络范畴。如果非要用进程pid做两用(既做调度进程管理,又在网络上标定主机的一个唯一进程),无疑是将进程管理和网络强耦合起来了。它可以但不合理。
-
在我们的系统中,并不是所有的进程都要进行网络通信的。而端口号是一种数字,标定当前主机上某一个唯一的进程,它更加的是一种证明,证明对应的进程是要进行网络通信的。没有端口号,这个进程只是本地间跑某些业务。而有端口号,一定是要对外的。
【问题】:底层如何通过port找到对应进程的?
- 实际底层采用哈希的方式建立了端口号和进程PID或PCB之间的映射关系,当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程。
【问题】:一个进程可以绑定多个端口号吗?
- 可以的。未来一个进程在进行网络通信的时候,它可能既和客户端A通信,也和客户端A的子模块通信,所以此进程就会绑定两个端口号。只要能够通过端口号找到同一个进程即可。但是一个端口号不能被多个进程绑定。因为端口号到进程具有唯一性。
网络通信
我们在网络通信的时候,只要让两台主机能够通信就可以了吗?
-
实际上,在进行通信的时候,不仅仅要考虑两台主机间互相交互数据。
-
本质上讲,进行数据交互的时候,是用户和用户在进行交互。用户的身份,通常是用程序体现的。程序一定是在运行中的 ---- 进程。
所以主机间通信的目的本质是:在各自的主机上的两个进程在互相交互数据。IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方。
所以:
-
IP —— 确保主机的唯一性
-
端口号(port)—— 确保该主机上的进程的唯一性
-
IP + PORT = 标识互联网中唯一的一个进程。—— socket
-
网络通信的本质:也是进程间通信
socket:
-
socket在英文上有“插座”的意思,插座上有不同规格的插孔,我们将插头插入到对应的插孔当中就能够实现电流的传输。
-
在进行网络通信时,客户端就相当于插头,服务端就相当于一个插座,但服务端上可能会有多个不同的服务进程(多个插孔),因此当我们在访问服务时需要指明服务进程的端口号(对应规格的插孔),才能享受对应服务进程的服务。
TCP协议
先简单介绍一下,之后再详细介绍
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议
先简单介绍一下,之后再详细介绍
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
总结:由于我们不能保证通信双方存储数据的方式是一样的,因此网络当中传输的数据必须考虑大小端问题。因此TCP/IP协议规定如下:网络数据流采用大端字节序,即低地址高字节。无论是大端机还是小端机,都必须按照TCP/IP协议规定的网络字节序来发送和接收数据。
- 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
- 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
- 需要注意的是,所有的大小端的转化工作是由操作系统来完成的,因为该操作属于通信细节,不过也有部分的信息需要我们自行进行处理,比如端口号和IP地址。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
-
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
-
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
-
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
-
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket编程接口
几个重要的函数接口
- socket套接字
- htons通常用于将主机字节顺序(host byte order)的16位整数转换为网络字节顺序(network byte order)
- inet_addr 通常用于将点分十进制的 IPv4 地址转换为网络字节顺序的 32 位整数
- sendto用于在UDP协议中发送数据的函数
- recvfrom用于在UDP协议中接收数据的函数
- socket常见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);
// 开始监听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结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。
-
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
-
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
-
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_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of 'struct sockaddr.' */
ussigned 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 in_addr
{
in_addr_t s_addr;
}
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
简洁版:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 地址家族 AF_INET
in_port_t sin_port; // 16位端口号,网络字节顺序
struct in_addr sin_addr; // 32位IPv4地址,网络字节顺序
char sin_zero[8]; // 不使用,填充0以使结构体大小与 struct sockaddr 相同
};
sockaddr_in
结构体包含以下字段:
sin_family
: 地址家族,通常为AF_INET
,表示IPv4地址。sin_port
: 16位端口号,以网络字节顺序存储,即大端字节序。sin_addr
: 32位IPv4地址,以网络字节顺序存储,即大端字节序。它是一个结构体struct in_addr
,其中包含一个字段s_addr
,表示IPv4地址。sin_zero
: 用于填充,以使sockaddr_in
的大小与通用地址结构struct sockaddr
相同。
在网络编程中,当使用套接字 API 中的函数时,
sockaddr_in
结构体通常需要进行类型转换,以便与通用的struct sockaddr
结构体一起使用。这是因为套接字函数(如bind
、connect
、recvfrom
、sendto
)通常使用通用地址结构struct sockaddr
来表示地址信息。在使用时,可以使用类型强制转换将sockaddr_in
转换为struct sockaddr
。
套接字编程的种类:
- 域间套接字编程——同一个机器内
- 原始套接字编程——网络工具
- 网络套接字编程——用户间的网络通信
网络接口统一抽象化:参数的类型必须是统一的。
简单的UDP网络程序
Log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
Main.cc
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: "<< proc << " port[1024+]\n" << std::endl;
}
std::string Handler(const std::string &str)
{
std::string res = "Server get a message: ";
res += str;
std::cout << res << std::endl;
return res;
}
std::string ExcuteCommand(const std::string &cmd)
{
//SafeCheck(cmd);
FILE *fp = popen(cmd.c_str(),"r");//文件中存放命令的执行结果
if(nullptr == fp)
{
perror("popen");
return "error";
}
std::string result;
char buffer[4096];
while(true)
{
char* p = fgets(buffer,sizeof(buffer),fp);
if(p == nullptr)break;
result += buffer;
}
pclose(fp);
return result;
}
//运行时的指令 ./udpserver port
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run(ExcuteCommand);//传递一个func指针;
return 0;
}
Makefile
.PHONY:all
all:udpserver udpclient
udpserver:Main.cc
g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpserver udpclient
UdpClient.cc
#include <iostream>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: "<< proc << " serverip serverport\n"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family= AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
cout<< "socker error!!!" <<endl;
return 1;
}
// client 也要bind,只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
string message;
char buffer[1024];
while(true)
{
cout<<"Please Enter@";
getline(cin, message);
//给某某发数据
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr *)&server,len);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
size_t s = recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);
if(s > 0)
{
buffer[s] = 0;
cout<<buffer<<endl;
}
}
close(sockfd);
return 0;
}
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 "Log.hpp"
//以下两种写法都可以
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport = 8080;
std::string default_ip = "0.0.0.0";
const int size = 1024;
class UdpServer{
public:
UdpServer(const uint16_t &port = defaultport, const std::string &ip = default_ip):sockfd_(0), port_(port), ip_(ip),isrunning_(false)
{}
void Init()
{
// 第一步 创建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_);
// 第二步 bind socket
struct sockaddr_in local;
bzero(&local, sizeof(local));//Linux里面的函数
local.sin_family = AF_INET;
local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的,htons函数用于将主机字节序(host byte order)的16位整数转换为网络字节序(network byte order)
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)//bind函数用于将一个套接字(socket)与特定的地址(IP地址和端口号)关联起来
{
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 Run(func_t func) // 对代码进行分层
{
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);//recvfrom是一个用于从网络套接字接收数据的函数
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string = func(info);//func为处理函数
sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 任意地址bind 0
uint16_t port_; // 表明服务器进程的端口号
bool isrunning_;
};
运行结果:
(不能使用设置了别名的命令)
window下网络编程套接字代码与Linux类似,添加对应的头文件,更改部分代码,比如:bzero函数要换成memset函数。