软件网站开发甘肃,毕业设计做购物网站的要求,58同城做网站多少钱,网页制作软件有那些文章目录#xff1a; 简单的UDP网络程序服务端创建套接字服务端绑定启动服务器udp客户端本地测试INADDR_ANY 地址转换函数关于 inet_ntoa 简单的UDP网络程序
服务端创建套接字
我们将服务端封装为一个类#xff0c;当定义一个服务器对象之后#xff0c;需要立即进行初始化… 文章目录 简单的UDP网络程序服务端创建套接字服务端绑定启动服务器udp客户端本地测试INADDR_ANY 地址转换函数关于 inet_ntoa 简单的UDP网络程序
服务端创建套接字
我们将服务端封装为一个类当定义一个服务器对象之后需要立即进行初始化服务器在 UDP 网络程序中初始服务器的第一个步骤就是创建套接字。我们使用 socket 函数创建套接字。
socket 函数的定义如下所示
int socket(int domain, int type, int protocol);参数说明
domain整数指定通信域Communication Domain。在同一主机上的进程之间进行通信时我们使用 POSIX 标准定义的 AF_LOCAL。在不同的主机通过 IPv4 连接的进程之间通信时我们使用 AF_INET对于通过 IPv6 连接的进程之间进行通信时我们使用 AF_INET6。type通信类型Communication Type。SOCK_STREAMTCP可靠的、面向连接的SOCK_DGRAMUDP不可靠的、无连接的。protocol互联网协议的协议值通常为0。这与数据包的 IP 头中的协议字段中显示的数字相同。
返回值套接字创建成功返回一个文件描述符否则返回-1同时错误码被设置。
在初始化服务器创建套接字时需要调用 socket 函数创建套接字我们需要填入的协议家族是 AF_INET(PF_INET)因为我们需要进行的是网络通信。服务器类型填入 SOCK_DGRAM 因为编写的 UDP 服务器是面向数据报的。第三个参数设置为0表示使用默认的传输协议。
下面是使用这些参数创建套接字的示例
#includeiostream
#includesys/socket.hclass UdpServer
{
public:void init(){// 创建套接字sockfd_ socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ 0){std::cerr Failed to create socket. std::endl;exit(1);}std::cout socket create success, sockfd: sockfd_ std::endl;// ......}
private:int sockfd_; // 文件描述符
};我们对其进行一个测试查看套接字的创建是否成功。
int main()
{UdpServer *svrnew UdpServer();svr-init();return 0;
}运行测试结果 服务端绑定
在套接字创建成功之后需要将其与网络相关联以便进行网络通信。对于一个 UDP 服务器绑定操作是必要的。通过绑定将套接字与指定的IP地址和端口号关联起来以便监听和处理该地址上的网络数据。
使用 bind 函数将套接字与特定的IP地址和端口号进行绑定bind 函数的定义如下
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明
sockfd需要绑定的套接字描述符。addr指向要绑定的地址结构的指针需要将其转化为 struct sockaddr 类型。addrlen地址结构长度。
返回值
绑定成功返回0。失败则返回-1且错误码被设置。
bind 函数的主要作用就是将套接字与指定的地址进行关联以便在该地址上进行监听和处理网络数据。调用 bind 之前需要确保套接字已经创建成功。
在绑定套接字之前需要定义一个 struct sockaddr_in 结构并将网络属性信息填充到该结构体中。由于 struct sockaddr_in 结构体中的一些字段是可选的建议在填充网络信息之前先将结构体变量进行清空然后填充协议家族、端口号、IP地址等信息。
下列示例展示了如何使用 bind 函数将套接字绑定到指定的IP地址和端口号
class UdpServer
{
public:UdpServer(int port 8080, std::string ip ): port_((uint16_t)port), ip_(ip), sockfd_(-1){}~UdpServer() {}void init(){// 1.创建套接字sockfd_ socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ 0){std::cerr Failed to create socket. std::endl;exit(1);}std::cout socket create success, sockfd: sockfd_ std::endl;// 2.绑定网络信息指明IPport// 2.1 先填充基本信息到 struct sockaddr_instruct sockaddr_in local; // lock在用户栈上开辟的空间 - 临时变量 - 写入内核中bzero(local, sizeof(local)); // bzerolocal.sin_family AF_INET; // 填充协议家族域local.sin_port htons(port_); // 填充服务器对应的端口信息一定会发给对方_port一定会到网络中// local.sin_addr // 服务器都必须具有IP地址(xx.yy.zz.aaa字符串风格点分十进制) - 4字节IP - uint32_t iplocal.sin_addr.s_addr ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());// 2.2 绑定网络信息if (bind(sockfd_, (struct sockaddr *)local, sizeof(local)) -1){std::cerr Failed to bind socket. std::endl;exit(2);}std::cout Socket bound successfully. std::endl;}private:uint16_t port_; // 服务器必须得有端口号信息std::string ip_; // 服务器必须得有IP地址int sockfd_; // 文件描述符
};启动服务器
UDP 服务器的初始化在上面已经完成。服务器初始化完成之后就可以启动服务器并提供服务了。服务器通常是以循环的方式运行的以便持续接收和处理客户端请求。UDP 服务器在接收到客户端发送的数据后可以直接读取这些数据而无需建立连接。
recvfrom函数 recvfrom 函数是在网络编程中使用的一个系统调用函数用于从一个指定的套接字接收数据并将数据存储到指定的缓冲区中。
recvfrom 函数的定义如下
#include sys/types.h
#include sys/socket.hssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);sockfd套接字描述符表示从该套接字描述符索引的文件中读取数据。buf指向接收数据的缓冲区。len期望读取数据的字节数。flags可选的标志参数用于控制接收操作的行为。常用的标志有0、MSG_DONTWAIT。src_addr指向用于存储发送方地址信息的 struct sockaddr 结构体的指针包括协议家族、IP地址、端口号等。addrlensrc_addr 结构体的长度及其可用空间大小这是一个输出型参数。
当使用 recvfrom 函数接收 UDP 数据时除了获取数据内容外还可以获取发送方的网络属性信息包括IP地址和端口号。调用 recvfrom 函数之前需要将 addrlen 参数设置为接收方地址结构体的大小以确保函数可以正确填充发送方的地址信息。
接下来使用下列函数启动服务器
当使用 recvfrom 函数读取客户端数据后可以将读取到的数据视为字符串并将最后一个位置设置为 ‘\0’ 以便于进行输出。此时我们也获取到了客户端的IP地址和端口号。recvfrom 函数调用成功之后返回的端口号是网络序列以大端字节序表示我们需要调用 ntohs 函数将其转换为主机序列与本地字节序相匹配。同样的获取的IP地址也需要用 inet_ntoa 函数进行转化。
class UdpServer
{
public:void start(){// 服务器设计的时候都是死循环char inbuffer[1024]; // 储存读取到的数据char outbuffer[1024]; // 储存发送的数据while (true){struct sockaddr_in peer; // 输出型参数socklen_t len sizeof(peer); // 输入型参数ssize_t s recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)peer, len);if (s 0)inbuffer[s] 0; // 将其当作字符串else if (s -1){std::cerr recvfrom: strerror(errno) : sockfd_ endl;continue; // 调用失败继续进行读取服务器不能因为一个客户端连接失败就退出}// 读取成功除了读取到对方的数据还要读取到对方的网络地址[ip,port]std::string peerIp inet_ntoa(peer.sin_addr);uint32_t peerPort ntohs(peer.sin_port);}}private:uint16_t port_; // 服务器必须得有端口号信息std::string ip_; // 服务器必须得有IP地址int sockfd_; // 文件描述符
};在构建服务器时我们可以引入命令行参数用于指定服务器的IP地址和端口号。使用云服务器实际上不需要传入IP地址直接运行程序时指定端口即可这里可以将IP地址设置为127.0.0.1它代表本地主机或本地环回地址相当于 localhost 。
以下为一个示例代码演示如何通过命令行参数传递端口号并将IP地址设置为本地环回地址
static void Usage(const std::string proc)
{cout Usage:\n\t proc port [ip] endl;
}// ./udpServer port [ip]
int main(int argc,char *argv[])
{if(argc!2argc!3){Usage(argv[0]);return 1;}std::string ip127.0.0.1;uint16_t portatoi(argv[1]);UdpServer svr(port,ip);svr.init();svr.start();
}如下所示运行程序并加上端口号就可以创建 udp 服务器成功了 使用 netstat 命令来查看当前网络的状态下图标出的就是当前运行的 udpServer 程序 netstat 是一个用于显示网络连接、路由表和网络接口等相关信息的命令。以下是 netstat 常用的选项
-a (all)显示所有的sockets包括监听和非监听的。-t (tcp)显示 TCP 协议相关的连接信息。-u (udp)显示 UDP 协议相关的连接信息。-n (numeric)以数字形式显示IP地址和端口号不进行主机名和服务名的解析。-p (program)显示与每个连接关联的进程/程序的 PID 和名称。-l (listening)显示每个处于监听状态的 sockets。
注意不同操作系统上的 netstat 命令可能会不同包括选项的名称和支持的功能。因此在使用 netstat 命令时可以通过 netstat --help 来查看系统文档使用说明。
udp客户端
udp客户端的实现步骤如下所示用于与服务器进行 UDP 通信
参数检查通过检查命令行参数来确保命令行输入的参数个数是否正确这是为了确保程序能够正确获取服务器的IP地址和端口号避免后续出现错误。获取服务端的IP地址和端口号根据命令行参数获取服务器的IP地址和端口号并将它们存储在变量 server_ip 和 server_port 中。这样客户端就获取到了需要连接服务器的地址信息。创建 UDP 套接字使用 socket 函数创建一个 UDP 套接字即创建一个用于网络通信的套接字。AF_INET 参数表示使用 IPv4 地址SOCK_DGRAM 参数表示使用数据报UDP 套接字类型。创建套接字成功之后会返回一个文件描述符用于后续该套接字的操作。设置服务器地址信息UDP 通信中需要指定连接的服务器地址信息。这里通过填充 server 结构体来实现。bzero 函数将 server 结构体清零。sin_family 表示地址族为 IPv4 sin_port 表示服务器的端口号sin_addr.s_addr 表示服务器的IP地址。
// ./udpServer server_ip server_port
// 客户端要连接服务端需要知道server对应的IP和port
class UdpClient
{
public:UdpClient(std::string ip , uint16_t port 8080): ip_(ip), port_(port), sockfd_(-1){}void init(){// 创建套接字sockfd_ socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ 0){std::cerr socket create error std::endl;exit(1);}}void start(){// 填写服务器对应信息struct sockaddr_in peer;bzero(peer, sizeof peer);peer.sin_family AF_INET;peer.sin_port htons(port_);peer.sin_addr.s_addr inet_addr(ip_.c_str());std::string buffer;// 启动客户端while (true){std::cout Please Enter# ;getline(cin, buffer);// 发送消息给serversendto(sockfd_, buffer.c_str(), buffer.size(), 0, (const struct sockaddr *)peer, sizeof(peer));}}~UdpClient(){if (sockfd_ 0)close(sockfd_);}private:std::string ip_;uint16_t port_;int sockfd_;
};static void Usage(const std::string name)
{cout Usage:\n\t name server_ip server_port endl;
}int main(int argc, char *argv[])
{if (argc ! 3){Usage(argv[0]);exit(2);}// 根据命令行设置需要访问的服务器IPstd::string server_ip argv[1];uint16_t server_port atoi(argv[2]);UdpClient client(server_ip, server_port);client.init();client.start();return 0;
}sendto函数
sendto 函数用于在 UDP 通信中发送数据它的函数定义如下
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 参数说明 sockfd使用 socket 函数创建的套接字描述符。 buf指向要发送的数据的缓冲区的指针。数据可以是任意类型的指针通常是一个 char 数组或字符串。 len要发送的数据的长度单位是字节。 flags指定发送操作的可选标志。一般使用0或与 MSG_DONTROUTE 等特殊标志进行按位或运算。有以下常用标志 MSG_DONTROUTE不使用路由表发送数据直接发送到目的地址。 MSG_OOB发送外带数据。 MSG_NOSIGNAL在发送数据时忽略 SIGPIPE 信号避免导致进程终止。 dest_addr对端网络相关的属性信息。在 UDP 通信中该参数指向 sockaddr 结构体的指针其中包含了目标服务器的IP地址和端口号。 addrlen传入 dest_addr 结构体的长度通常是 sizeof(struct sockaddr_in)。
本地测试
udp 的服务端和客户端的代码都已经编写完成。因此我们可以先在本地进行测试使用本地环回地址127.0.0.1作为服务器的IP地址客户端可以连接到该地址来与服务器进行通信。同时我们需要确保服务器绑定的端口号与客户端连接时指定的端口号一致。 当客户端和服务端成功建立连接并进行通信时可以通过使用 netstat 命令来看网络信息来确认连接的建立。 INADDR_ANY INADDR_ANY 是一个IP地址当我们不想将套接字绑定到任何特定的IP时使用。在实现通信时我们需要将套接字绑定到IP地址。当我们不知道自己及其的IP地址或者其它情况时可以使用特殊的IP地址 INADDR_ANY。它允许我们的服务器接收任何接口作为目标的数据包。 在进行网络测试时你需要确保服务器能够通过公网IP地址进行访问。然而直接将服务器绑定到公网IP可能会导致绑定失败的问题。 在云服务器中你所获得的公网IP地址并不一定是真正的公网IP而是有云服务器厂商提供的内部IP地址这些内部IP地址无法直接在服务器代码中进行绑定。
为了让服务器能够通过公网IP进行访问可以使用一个特殊的绑定值即 INADDR_ANY 。这个值是系统提供的宏对应的数值为0。通过将服务器绑定到 INADDR_ANY 就可以让服务器接收来自任意IP地址的连接请求。
若想要我们写的服务端能够被外部网络进行访问我们可以这样做
在服务器代码中将与IP地址相关的代码去掉。这样可以使服务器动态绑定到可用的IP地址。填充 struct sockaddr_in 结构体时将IP地址设置为 INADDR_ANY 。这样服务器就可以接收到来自任意IP地址的连接请求了。
struct sockaddr_in peer;
bzero(peer, sizeof peer);
peer.sin_family AF_INET;
peer.sin_port htons(port_);
peer.sin_addr.s_addr INADDR_ANY;注意INADDR_ANY 的值是0它不需要进行网络字节序的转化。因此在设置时无需进行大小端的处理。 编译并运行修改后的代码时再次使用 netstat 命令查看网络连接情况时该服务器的IP地址变成了 0.0.0.0 意味着该服务器可以接收任意主机发起的连接请求 地址转换函数
这里主要介绍的是 IPv4 的 socket 网络编程。sockaddr_in 中的成员 struct in_addr sin_addr 表示32位的IP地址。但是我们通常使用点分十进制的字符串表示IP地址以下函数可以在字符串表示和 in_addr 表示之间进行转换
字符串转 in_addr 的函数
#include arpa/inet.h// 将字符串cp表示的IP地址转换为in_addr结构体并将结果存储在inp中
int inet_aton(const char *cp, struct in_addr *inp);// 将字符串cp表示的IP地址转换为in_addr_t类型的值
in_addr_t inet_addr(const char *cp);// 将字符串从src表示的IP地址转换为指定地址族af的二进制表示并将转换后的结果存储在dst中
int inet_pton(int af, const char *src, void *dst);in_addr转字符串的函数
// 将in_addr结构体中的IP地址转换为字符串形式
char *inet_ntoa(struct in_addr in);// 将指定地址族af的二进制表示src转化为字符串形式并将结果存储在dst中
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr ,还可以转换 IPv6 的 in6_addr 因此函数接口是 void *addrptr 。
关于 inet_ntoa
inet_ntoa 这个函数的返回值类型是 char*很显然这个函数在自己内部申请了一块内存来报错IP结果那么是否需要调用者手动释放呢 man 手册上说inet_ntoa 函数是把这个结果放在了静态存储区。这时不需要我们进行手动释放。那么若多次调用该函数会有什么样的效果呢
请看代码
#include iostream
#include netinet/in.h
#include arpa/inet.h
using namespace std;int main()
{struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr 0;addr2.sin_addr.s_addr 0xffffffff;char *ptr1 inet_ntoa(addr1.sin_addr);char *ptr2 inet_ntoa(addr2.sin_addr);cout ptr1: ptr1 ptr2: ptr2 endl;return 0;
}运行结果如下所示 由于 inet_ntoa 函数使用了静态缓冲区每次调用该函数时返回的指针都会指向同一个缓冲区。这意味着如果在多个地方同时使用了 inet_ntoa 返回值并且后续的调用覆盖了之前的结果那么之前获取的字符串指针将变为无效。
思考如果有多个线程调用 inet_ntoa是否会出现异常情况呢?
在 APUE 中明确提出了 inet_ntoa 不是线程安全的函数。因为它使用了一个全局共享的缓冲区来保存转化后的字符串。所以多线程环境下该函数可能会导致竞争条件从而出错。但是在 centos7 上测试并没有出现问题这可能是在内部加了互斥锁来保证线程安全性。在多线程环境下推荐使用 inet_ntop 函数它是线程安全的。inet_ntop 函数要求调用者提供一个缓冲区来存储转换后的字符串避免了静态缓冲区的共享和竞争条件可以规避线程安全的问题。
多线程调用 inet_ntoa 代码示例如下
#include stdio.h
#include unistd.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include pthread.hvoid *Func1(void *p)
{struct sockaddr_in *addr (struct sockaddr_in *)p;while (1){char *ptr inet_ntoa(addr-sin_addr);printf(addr1: %s\n, ptr);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr (struct sockaddr_in *)p;while (1){char *ptr inet_ntoa(addr-sin_addr);printf(addr2: %s\n, ptr);}return NULL;
}int main()
{pthread_t tid1 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr 0;addr2.sin_addr.s_addr 0xffffffff;pthread_create(tid1, NULL, Func1, addr1);pthread_t tid2 0;pthread_create(tid2, NULL, Func2, addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}在该程序测试中并没有出现问题但这并不意味着 inet_ntoa 函数在多线程环境中是安全的。为了确保线程安全仍然建议使用线程安全的函数 inet_ntop 来替代 inet_ntoa 。