目录
- `Socket编程准备知识`
- `理解源IP地址和目的IP地址 `
- `认识端口号 `
- `网络字节序 `
- `socket编程`
- `socket编程接口`
- `socket系统调用`
- `bzero函数`
- `struct sockaddr结构体分类`
- `以sockaddr_in为例`
- `bind系统调用`
- `inet_addr函数与inet_ntoa函数`
- `recvfrom系统调用`
- `sendto系统调用`
- `udpsocket代码练习`
- `Server.hpp`
- `main.cc`
- `Client.cc`
- `InetAddr.hpp`
Socket编程准备知识
理解源IP地址和目的IP地址
- 在IP数据包头部中, 有两个IP地址, 分别叫做
源IP地址
, 和目的IP地址
.
思考: 我们光有IP地址就可以完成通信了吗? 想象一下发qq消息的例子,,有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出,这个数据要给哪个程序进行解析。
认识端口号
- 端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数(
uint16_t
)。 端口号用来标识一个进程
, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
。- 一个端口号只能被一个进程占用。
注意:
一个进程可以绑定多个端口号, 但是一个端口号不能被多个进程绑定。- 传输层协议(TCP和 UDP)的数据段中有两个端口号,分别叫做
源端口号
和目的端口号
.就是在描述 “数据是谁发的,要发给谁”;
端口号范围划分
- 0 - 1023:知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议,他们的端口号都是固定的.
- 1024 - 65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的.
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 在网络编程中,网络字节序统一规定为
大端序
。
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket编程
在网络编程中,Socket 是一种非常重要的抽象,它提供了在不同主机之间或同一主机上的不同进程之间进行通信的端点。Socket 编程是网络通信的基础,尤其是在 TCP/IP 协议栈
中。
socket编程接口
socket系统调用
功能
:主要作用是创建一个新的socket(套接字)
,这个socket是网络通信中的一个端点,用于实现不同主机之间或同一主机上不同进程之间的数据交换。Domain(域)
:指定 Socket 所使用的协议族
。对于 Internet 上使用的 TCP/IP 协议,该参数通常是 AF_INET(IPv4)或 AF_INET6(IPv6)。其他可能的值包括 AF_UNIX(Unix 域套接字,仅在同一台机器上的不同进程间通信时使用)。Type(类型)
:指定Socket 的类型
。常用的类型有 SOCK_STREAM(表示面向连接的 TCP 协议)和 SOCK_DGRAM(表示无连接的 UDP 协议)。还有其他的类型,如 SOCK_RAW(原始套接字,可以接收 IP 层的数据包),但这些较为特殊。Protocol(协议)
:在大多数情况下,对于 TCP 和 UDP,可以将此参数设置为 0,让系统自动选择默认的协议。- 成功时:socket() 函数成功执行后,会返回一个非负整数作为 Socket 描述符。这个描述符在后续的所有 Socket 操作中都会被用到。可以理解为一个
文件描述符fd
。 - 失败时:如果 socket() 函数执行失败,它会返回 -1,并设置全局变量 errno 以指示错误的具体原因。
bzero函数
- 作用是
将指定内存区域的前n个字节置为零
。 - void *s:指向需要清零的内存区域的起始地址的指针。
- size_t n:需要清零的字节数。这个参数指定了从s指向的内存地址开始,要清零的字节数量。
- 该作用也可以使用
memset函数
实现。
struct sockaddr结构体分类
以sockaddr_in为例
代码示例:
// 构建 sockaddr_in结构体并初始化
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空addr 也可以使用 memset(&local,0,sizeof(local))
local.sin_family = AF_INET;
local.sin_port = htons(_Serverport); // 主机序列转网络序列
local.sin_addr.s_addr = inet_addr(_Serverip.c_str()); // IP字符串转为int,并且转为网络序列
bind系统调用
- bind 函数的作用是
将一个套接字(Socket)与一个特定的IP地址和端口号(对于TCP/IP协议)绑定起来
,或者与一个特定的路径名(对于Unix域套接字)绑定起来。 sockfd:
这是一个由socket函数返回的套接字描述符。它标识了要进行绑定的套接字。addr:
这是一个指向sockaddr
结构(或其特定于协议的变体,如sockaddr_in
对于IPv4)的指针。这个结构包含了要绑定的IP地址和端口号。对于IPv4,通常使用sockaddr_in结构
,它包含了一个sin_family字段
(指定地址族,如AF_INET),一个sin_addr字段
(包含IP地址),以及一个sin_port字段
(包含端口号,以网络字节顺序表示)。注意:
使用的时候都会强转为sockaddr类型。addrlen:
这是一个表示addr参数所指向的地址结构长度的整数。这允许bind函数知道传递给它的是哪种类型的地址结构。- 调用成功返回0,失败返回-1,错误码被设置。
注意:客户端需要绑定,但是不需要调用bind显示的绑定,udp客户端首次发送消息的时候,OS会自动随机给客户端进行端口号的绑定,原因:要绑定,必然要和本地的特定的端口号相关联起来,但是一个端口号只能和一个进程相关联,这样做可以防止客户端端口号的冲突。
我们使用的云服务器不允许bind公网ip,也不推荐绑定公网IP或任意一个确定的ip,推荐在绑定IP的时候,将ip设置为0(或则宏 INADDR_ANY
),即任意IP地址绑定。
代码示例:
// 构建 sockaddr_in结构体并初始化
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空addr 也可以使用 memset(&local,0,sizeof(local))
local.sin_family = AF_INET;
local.sin_port = htons(_Serverport); // 主机序列转网络序列
local.sin_addr.s_addr = inet_addr(_Serverip.c_str()); // IP字符串转为int,并且转为网络序列
socklen_t len;
len = sizeof(local);
// bind 绑定ip 与 port
bind(_socketfd, (struct sockaddr *)&local, len);
inet_addr函数与inet_ntoa函数
inet_addr函数
的主要作用是将一个用点分十进制
(例如"192.168.1.1")格式的IPv4地址字符串转换
成一个无符号长整型数
(u_long类型),并且这个长整型数已经按照网络字节顺序排列
。网络字节顺序通常指的是大端字节序,这是在网络通信中广泛采用的一种字节序标准。(inet_addr函数只能处理IPv4地址,无法处理IPv6地址
)inet_ntoa函数
的作用与inet_addr函数相反,它将一个表示IPv4地址的无符号长整型数
(u_long类型,网络字节顺序)转换回用点分十进制格式表示的字符串
。(inet_ntoa函数也只能处理IPv4地址)
recvfrom系统调用
- recvfrom在Linux中
用于接收数据的系统调用
。它特别适用于面向无连接的协议,如UDP(用户数据报协议)。recvfrom函数允许程序接收数据,并同时获取数据发送方的地址信息。 - recvfrom函数的返回值类型为ssize_t,即有符号的size_t类型。这种类型可以表示正值、负值和零,用于表示接收到的数据大小或错误信息。
- 成功时:返回值为接收到的数据字节数。
- 失败时:返回值为-1,此时可以通过检查errno来获取具体的错误原因。
sockfd:
需要接收数据的socket文件描述符。这是之前通过socket()系统调用创建的。buf:
指向接收数据的缓冲区的指针。接收到的数据将被存储在这个缓冲区中。len:
缓冲区的大小,即可以接收的最大数据量。如果接收到的数据量超过了这个大小,数据可能会被截断
。flags:
指定接收数据的方式和行为。这个参数可以包含多个标志位,用于修改函数的行为。常见的标志位包括MSG_DONTWAIT
(非阻塞模式,如果没有数据可读,则立即返回)和MSG_WAITALL
(阻塞模式,直到接收到指定长度的数据才返回)。src_addr:
含义:指向一个sockaddr结构体的指针,该结构体用于存储发送方的地址信息
。在调用recvfrom之前,这个参数可以设置为NULL,如果不关心发送方的地址信息。但是,如果提供了非NULL的指针,recvfrom会在接收数据的同时,将发送方的地址信息填充到这个结构体中。addrlen:
含义:指向一个socklen_t 变量的指针,该变量用于存储发送方地址的长度。在调用recvfrom之前,应该将这个变量的值设置为src_addr所指向的sockaddr结构体的大小。在调用之后,recvfrom会更新这个变量的值,以反映实际存储的发送方地址的长度。
示例代码:
char buffer[1024];
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len;
len = sizeof(peer);
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if(n<0)
{
exit(1);
}
sendto系统调用
sendto函数
是Linux系统(以及类Unix系统)中用于在网络上发送数据的函数之一
。该函数允许程序将数据发送到指定的目标地址,目标地址包括目标主机的IP地址和端口号。- 成功时:返回值是实际发送的数据字节数。如果数据被完全发送,则返回值等于请求发送的字节数。
- 失败时:返回值是-1,表示发送数据失败。
- sockfd:socket文件描述符,它是通过socket()系统调用创建的。
- buf:指向要发送数据的缓冲区的指针。
- len:指定msg指向的缓冲区中数据的长度(以字节为单位)。
- flags:指定sendto函数的行为。这个参数通常设置为0,表示使用默认行为。
- dest_addr:指向sockaddr结构体的指针,该
结构体包含了目标地址的信息
,包括目标主机的IP地址和端口号。这是sendto函数将数据发送到的目的地。 - addrlen:指定 dest_addr 指向的sockaddr结构体的大小。这个参数用于确保sendto函数能够正确地解释目标地址信息。
代码示例:
std::string message;
std::cout << "Please Enter#" << std::endl;
getline(std::cin, message); //键盘获取信息
struct sockaddr_in Server; //创建一个sockaddr_in结构体
bzero(&Server, sizeof(Server));
//初始化
Server.sin_family = AF_INET;
Server.sin_port = htons(Serverport);
Server.sin_addr.s_addr = inet_addr(ServerIp.c_str());
sendto(socketfd, message.c_str(), sizeof(message), 0, (struct sockaddr *)&Server, sizeof(Server));
udpsocket代码练习
Udp_echo_Server(Client向Server发送什么就返回什么)
Server.hpp
#pragma
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <cstring>
#include "Log.hpp"
#include"InetAddr.hpp"
using namespace std;
enum EXITERROR
{
SOCKETCREATE = 1,
SOCKETBIND
};
class UdpServer
{
public:
UdpServer( uint16_t port) : _Serverport(port),/* _Serverip(ip)*/ _IsRuning(false), _socketfd(-1)
{}
void UdpServer_init()
{
// 创建 socket套接字
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd == -1) // 创建失败
{
LOG(FATAL, "socket fialed...cerrno:%d strerror:%s", errno, strerror(errno));
exit(SOCKETCREATE);
}
LOG(DEBUG, "socket create success..., _socketfd is: %d", _socketfd);
// 构建 sockaddr_in结构体并初始化
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空addr 也可以使用 memset(&local,0,sizeof(local))
local.sin_family = AF_INET;
local.sin_port = htons(_Serverport); // 主机序列转网络序列
//local.sin_addr.s_addr = inet_addr(_Serverip.c_str()); // IP字符串转为int,并且转为网络序列
local.sin_addr.s_addr = INADDR_ANY;//0
socklen_t len;
len = sizeof(local);
// bind 绑定ip 与 port
int n = bind(_socketfd, (struct sockaddr *)&local, len);
if (n == -1)
{
LOG(FATAL, "bind socket fialed... ,cerrno: %d ,strerror: %s", errno, strerror(errno));
exit(SOCKETBIND);
}
LOG(DEBUG, "bind socket success...");
}
void UdpServer_Start()
{
_IsRuning = true;
while (true)
{
// 1. 接受Client的信息
char buffer[1024];
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len;
len = sizeof(peer);
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if (n < 0)
{
LOG(DEBUG, "UdpServer receive message error,errno:%d ,strerrno:%s", errno, strerror(errno));
}
InetAddr addr(&peer);
cout << "UpdServer receive a message is:" << buffer<<" from: "<<addr.GetIP()<<" "<<addr.Getport() << endl;
// 2. 将获取的信息再返回回去
sendto(_socketfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);
}
_IsRuning = false;
}
~UdpServer()
{}
private:
int _socketfd;
uint16_t _Serverport;
// string _Serverip;
bool _IsRuning;
};
main.cc
#include <memory>
#include <iostream>
#include "UdpServer.hpp"
void Usage(string str)
{
std::cout << "Usage:\n\t "<< str << " Serverport" << std::endl;
}
// ./Server Serverport
int main(int argc, char *args[])
{
if (argc != 2)
{
Usage(args[0]);
exit(1);
}
uint16_t Serverport = std::stoi(args[1]);
EnIsPrint();
std::unique_ptr<UdpServer> Serptr = std::make_unique<UdpServer>(Serverport); //c++14语法
Serptr->UdpServer_init();
Serptr->UdpServer_Start();
return 0;
}
Client.cc
#pragma
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
void Usage(std::string str)
{
std::cout << "Usage:\n\t " << str << " Serverip Serverport" << std::endl;
}
// ./UdpClient Serverport
int main(int argc, char *args[])
{
if (argc != 3)
{
Usage(args[0]);
exit(1);
}
uint16_t Serverport = std::stoi(args[2]);
// std::string ServerIp = args[1];
// 创建套接字
int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (socketfd == -1)
{
std::cout << "Client socket create failed..." << std::endl;
exit(1);
}
struct sockaddr_in Server;
bzero(&Server, sizeof(Server));
Server.sin_family = AF_INET;
Server.sin_port = htons(Serverport);
// Server.sin_addr.s_addr = inet_addr(ServerIp.c_str());
Server.sin_addr.s_addr = 0;
// std::string message;
// Client不需要显示的bind
while (true)
{
std::string message;
std::cout << "Please Enter#" << std::endl;
getline(std::cin, message); //键盘获取信息
sendto(socketfd, message.c_str(), sizeof(message), 0, (struct sockaddr *)&Server, sizeof(Server));
struct sockaddr_in Server;
socklen_t size = sizeof(Server);
char buffer[1024];
ssize_t n = recvfrom(socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&Server, &size);
if (n > 0)
{
buffer[n] = 0;
std::cout << "Server echo# " << buffer << std::endl;
}
}
return 0;
}
InetAddr.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr
{
private:
void GetAddr(std::string *ip, uint16_t *port)
{
*ip = inet_ntoa(_Addr->sin_addr);
*port = ntohs(_Addr->sin_port);
}
public:
InetAddr(struct sockaddr_in *Addr): _Addr(Addr)
{
GetAddr(&_ip,&_port);
}
std::string GetIP()
{
return _ip;
}
uint16_t Getport()
{
return _port;
}
~InetAddr()
{
}
private:
struct sockaddr_in *_Addr;
std::string _ip;
uint16_t _port;
};