一、Socket编程预备
1.1 理解通行的本质
在网络通信中将数据传输到主机是目的吗?其实并不是,在现实生活中,我们通过网络聊天是人与人在聊天,下载软件是人在下载,浏览网页是人在浏览,而我们聊天用的微信、下载用的应用商店、浏览用的浏览器在主机中是不同的进程,所以人在主机中的体现就是一个进程,网络通信的本质就是一个主机的进程将信息发送到另一个主机的进程,即网络通信的本质依旧是进程间通信。数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程,才是目的。而在一个主机中一定存在许多的进程,这就需要一个东西来表明进程的唯一性,在Linux系统编程中我们知道一个pid可以唯一标识一个进程,但是在网络中,设计师引入了另一个元素来唯一标识进程---端口号(port),其原因是想减少网络与系统的耦合性,便于维护管理
1.2 端口号
端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址+端口号能够标识网络上的某一台主机的某一个进程,我们把ip+port叫做套接字socket
- 一个端口号只能被一个进程占用.
端口号范围划分
- 0 - 1023:知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议,他们的端口号都是固定的,就像我们日常生活中110表示报警电话一样
- 1024 - 65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的
1.3传输层协议
UDP协议
UDP协议是一种无需连接的不可靠的面向数据报的传输层协议
由于UDP是不可靠的,当发生丢包问题时,UDP是无法解决的
TCP协议
TCP协议是一种需要连接的可靠的面向字节流的传输层协议
使用TCP协议进行数据通信时,两台主机需要先进行连接才能通信,并且TCP协议是可靠的协议,当发生丢包的问题时,TCP会有对应的应对方法
ps:可靠与不可靠不是两个协议的优缺点而是特点,UDP协议是不可靠的注定它的实现和使用是相对容易的,TCP协议虽然是可靠的但是也注定其实现和使用是相对麻烦的,故两个协议没有好坏之分,在不同的场景下两个协议各有各的优点
1.4 网络字节序
在计算机中数据的保存存在大小端之分
- 小端:低字节的数据保存在低地址处,高字节的数据保存在高地址处
- 大端:低字节的数据保存在高地址处,高字节的数据保存在低地址处
- 口诀:小小小
如果数据仅在本机中传输,就不用管大小端的问题,因为一个机器的存储方式是一样的,但是如果是不同主机之间进行通信,那就需要考虑大小端的问题了,因为不同机器数据的存储方式可能不同,不考虑大小端就可能造成数据的读写错误
TCP/IP协议也考虑到了这个问题,规定网络数据流应采用大端字节序,即低地址高字节
不管这台主机是大端机还是小端机,都会按照这个TCP/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位短整数。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
1.5.socket编程接口
// 创建 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);
1.6 sockaddr结构
在网络编程中,为了处理不同类型的网络地址(如IPv4、IPv6等),引入了sockaddr结构体。然而,sockaddr结构体本身是一个通用的、未特定化的结构,它包含了一个地址族(sa_family)和一个地址数据缓冲区(sa_data)。由于sa_data是一个字符数组,直接操作它并不方便,因此在实际使用中,通常会使用sockaddr的派生结构体,如sockaddr_in(sockaddr_in6 用于IPv6)和 sockaddr_un结构体。
sockaddr_in用于网络通信,sockaddr_un用于本地通信。
设计师为了让我们在进行网络同信和本地通信时使用同一批接口,于是引入了sockaddr结构,该结构体与sockaddr_in和sockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这样只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容,socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in,这样的好处是程序的通用性,可以接收IPv4, IPv6,以及UNIX DomainSocket各种类型的sockaddr结构体指针做为参数;这样做类似于多态
二、UDP套接字的使用
2.1创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
- domain:指定使用的协议族。常见的取值有
AF_INET
(IPv4)和AF_INET6
(IPv6)。这个参数决定了地址的格式和类型。 - type:指定套接字的类型。常见的取值有
SOCK_STREAM
(流套接字,提供有序的、可靠的、双向的和基于连接的字节流,通常使用TCP协议)和SOCK_DGRAM
(数据报套接字,支持无连接的、不可靠的和使用固定大小缓冲区的数据报服务,通常使用UDP协议)。 - protocol:指定协议编号。通常可以设置为0,让系统根据domain和type自动选择合适的协议。
返回值:
- 成功:socket函数成功执行时,返回一个非负整数,续的socket编程中用于标识和操作该套接字即套接字的文件描述符。这个描述符在后。
- 失败:如果socket函数调用失败,将返回-1,并设置相应的errno以指示错误原因。
2.2 绑定
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind函数的作用是将一个套接字与一个具体的地址(包括IP地址和端口号)绑定。这样,当套接字进行通信时,就可以使用这个指定的地址作为通信的源地址。
参数
- sockfd:标识一个已经创建但尚未绑定的套接字的文件描述符。
- addr:指向一个包含地址信息的结构体的指针。对于IPv4,通常使用
struct sockaddr_in
;对于IPv6,则使用struct sockaddr_in6
。 - addrlen:
addr
结构体的大小,通常可以使用sizeof
操作符获取。
返回值:
- 成功时,
bind
函数返回0。 - 失败时,返回-1,并设置相应的errno以指示错误原因。
void Init()
{
//创建UDP文件
_socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(_socketfd<0)
{
LOG(FATAL,"create socket error\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG,"create socket success,socketfd:%d\n",_socketfd);
//bind
struct sockaddr_in local;
memset(&local,0,sizeof(local));//使用该结构提前先初始化一下
local.sin_family=AF_INET;
local.sin_port=htons(_localport);//改为网络字节序
local.sin_addr.s_addr=INADDR_ANY;
int n=bind(_socketfd,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
LOG(FATAL,"bind error\n");
exit(BIND_ERROR);
}
LOG(DEBUG,"bind success!\n");
}
struct sockaddr_in结构体:
sin_family:这是一个地址族(address family)的标识,通常设置为
AF_INET
,表示IPv4地址族。sin_port:这是一个16位的无符号整数,表示端口号。端口号在网络传输中用于区分不同的服务或应用程序。在赋值时,需要使用网络字节序(big-endian),这通常通过
htons
函数进行转换。sin_addr:这是一个
struct in_addr
类型的结构体,用于表示IPv4地址。struct in_addr
内部通常包含一个unsigned long
类型的成员s_addr
,用于存储IP地址(同样以网络字节序表示)。sin_zero:这是一个填充字段,通常用于确保
sockaddr_in
结构体的大小与更通用的sockaddr
结构体的大小相同。在实际使用中,这个字段通常被设置为0。不过,在较新的系统或库中,这个字段可能不再存在,因为sockaddr_in
已经通过其他方式(如结构体对齐)来确保大小的一致性。定义在<netinet/in.h>头文件中
注意事项:
网络字节序与主机字节序:在处理IP地址和端口号时,需要注意网络字节序与主机字节序的区别。网络字节序是大端字节序,而主机字节序则可能是大端或小端。因此,在赋值时需要使用相应的转换函数(如
htons
、htonl
等)来确保数据的正确性。结构体大小:由于
sockaddr_in
结构体的大小可能因编译器或平台的不同而有所差异,因此在某些情况下可能需要使用socklen_t
类型的变量来存储结构体的大小,以确保兼容性。
补充:
- 一般服务端不会绑定一个指定的IP地址,因为如果服务端在绑定的时候是指明绑定一个具体的IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据
- IP地址分为两种字符串形式的与32位整数形式的,类似于127.0.0.1字符串形式的是给我们看的,整数形式的是用于计算机网络传输用的,分为两种的原因是因为对于字符串来说我们使用来更加清晰,对于计算机处理和计算来说32位整数计算起来更加快捷,所以会有两种形式,系统还提供了一些系统调用来实现两种形式的转化
字符串形式转整数形式:
in_addr_t inet_addr(const char *cp);
整数形式转字符串形式:
char *inet_ntoa(struct in_addr in);
2.3 接受数据与发送数据
UDP套接字相比于TCP套接字使用起来更加简单,绑定完就可以使用了,我们可以通过socket返回的文件描述符来接受发送信息,实现业务,由于UDP协议是面向数据报的协议,而我们在文件使用的read、write系统调用是面向字节流的,所以UDP网络套接字有两个固定的接受发送函数
recvfrom
ssize_t recvfrom(int sockfd , void *buf
, size_t len , int flags
, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
- sockfd:绑定的文件描述符
- buf:读取的数据所存放的缓冲区
- size_t:想要读取的字节数
- flags:读取数据的方式,一般情况下设置为0,表示阻塞读取,通过设置flags参数为MSG_DONTWAIT来使recvfrom变为非阻塞模式,此时如果没有数据可读,函数将立即返回。
- 最后两个参数为输出型参数,可以通过他们来接受是谁发送的信息
sendto
ssize_t sendto(int sockfd , const void *buf
, size_t len, int flags
, const struct sockaddr *dest_addr , socklen_t addrlen);
- 地址信息:在使用
sendto
函数时,需要将目的地址和端口号封装成sockaddr_in
结构体,并将其地址作为dest_addr
参数传递给函数。 - 缓冲区大小:发送缓冲区的大小应该足够大,以容纳要发送的数据。如果数据过大,可能需要分多次发送。
- 错误处理:如果
sendto
函数返回-1,表示发送失败。此时,可以通过检查errno
变量来获取具体的错误信息。 - 非阻塞模式:可以通过设置
flags
参数为MSG_DONTWAIT
来使sendto
函数在非阻塞模式下工作。在非阻塞模式下,如果套接字没有准备好发送数据,函数将立即返回-1,并设置errno
为EAGAIN
或EWOULDBLOCK
。
void Start()
{
_isrunning=true;
char buffer[1024];
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int n=recvfrom(_socketfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
InetAddr addr(peer);
buffer[n]='\0';
std::cout<<"["<<addr.IP()<<":"<<addr.Port()<<"]#"<<buffer<<std::endl;
std::string echo="[udp_server echo]:";//这里我们实现的功能是客户端发送什么信息服务端就给客户端在发送回去
echo+=buffer;
sendto(_socketfd,echo.c_str(),echo.size(),0,(struct sockaddr*)&peer,len);
}
else
{
std::cout << "recvfrom , error" << std::endl;
}
}
}
ps:上述代码中的Inet_Addr是我们自己封装的一个类,因为我们想知道客户端的ip地址和端口号等信息
//Inet_Addr.hpp
class InetAddr
{
public:
void ToHost(const struct sockaddr_in &addr)
{
_port = ntohs(addr.sin_port);
_ip = inet_ntoa(addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in& addr)
:_addr(addr)
{
ToHost(addr);
}
std::string IP()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
private :
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
2.4 客户端
客户端的实现与服务端类似,都需要创建socket套接字,不同的是客户端不需要显示的调用bind函数,我们只需要将服务端的ip地址和端口号等信息封装到sockaddr_in结构体中即可,OS会自动绑定,一般客户端一定知道服务端的ip地址和端口号,所以我们可以利用命令行参数来接受,我们执行时一般形式:./udp_client 127.0.0.1 8888
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include "Log.hpp"
#include "InetAddr.hpp"
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while (true)
{
std::string message;
std::cout << "Please Input:";
std::getline(std::cin, message);
int n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
if(n>0)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
else
{
std::cout << "recvfrom error" << std::endl;
break;
}
}
else
{
cout<<"send error"<<endl;
break;
}
}
close(sockfd);
return 0;
}
2.5 测试代码
客户端:
服务端:
ps:127.0.0.1是一个特殊的 IP 地址,被称为环回地址(Loopback Address)。在计算机网络中,它被用于指向本机(即运行该 IP 地址的计算机)。当你尝试访问 127.0.0.1时,实际上是在与运行该 IP 地址的计算机上的应用程序进行通信,而不是通过网络与其他计算机通信。
标签:sockaddr,UDP,addr,字节,int,网络,接字,端口号,struct From: https://blog.csdn.net/m0_74910646/article/details/141036107