目录
[1.1.创建udp socket 套接字 --- 必须要做的](#1.1.创建udp socket 套接字 --- 必须要做的)
[1.3.bind sockfd和网络信息(IP + Port)](#1.3.bind sockfd和网络信息(IP + Port))
[2.1. 我们要让server先收数据](#2.1. 我们要让server先收数据)
[recvfrom 函数](#recvfrom 函数)
[2.2. 我们要将server收到的数据,发回给对方](#2.2. 我们要将server收到的数据,发回给对方)
前言:
我们之前讲解了关于socket编程的一些基础知识和接口函数,今天我们就来小试牛刀一下,自己编写一个简单的echo_server程序,将客户端的数据在服务端打印出来(利用udp协议实现)!
1.实现udpserver类
1.1.创建udp socket 套接字 --- 必须要做的
socket()讲解
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket函数是一个系统调用函数,用于创建一个新的套接字。该函数返回一个套接字文件描述符,如果创建失败则返回-1。
参数讲解:
domain: 选择通信方式 --- 本地通信与网络通信
type: 选择协议--- UDP/TCP
protocol: 默认使用0、
返回值是创建的socket文件操作符socketfd
代码实现:
代码讲解:
-
AF_INET
:这是socket()
函数的第一个参数,指定了地址族(Address Family)。AF_INET
表示使用IPv4地址,它是Internet地址族的简写。 -
SOCK_DGRAM
:这是socket()
函数的第二个参数,指定了套接字类型。SOCK_DGRAM
表示数据报套接字,这是一种无连接的、固定最大长度的消息服务。它常用于UDP(用户数据报协议)通信。 -
0
:这是socket()
函数的第三个参数,通常用于指定协议。当使用AF_INET和SOCK_DGRAM时,这个参数通常为0,表示自动选择对应的协议(在这种情况下是UDP)。
1.2.填充sockaddr_in结构
sockaddr_in结构体通常用于网络编程中表示IPv4地址和端口便于我们进行网络通信。
sockaddr_in
是一个在<netinet/in.h>
(或<arpa/inet.h>
,取决于您的系统)头文件中定义的结构体,用于存储IPv4地址和端口信息。
代码实现:
代码解析:
htons()函数将主机序列,转成网络序列,填充sockaddr_in结构
1.3.bind sockfd和网络信息(IP + Port)
bind函数解析:
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
bind绑定 ,将socket文件与IP地址绑定和端口号,也就是将进程与文件进行绑定。这样当数据包到达该端口和地址时,操作系统知道应该将数据传递给哪个应用程序。
代码展现:
1.4.总代码
cpp
void InitServer()
{
// 1. 创建udp socket 套接字 --- 必须要做的
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(FATAL, "socket error, %s, %d\n", strerror(errno), errno);
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd: %d\n", _sockfd);
// 2.0 填充sockaddr_in结构
struct sockaddr_in local; // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。int a = 100; a = 20;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // port要经过网络传输给对面,先到网络,_port:主机序列-> 主机序列,转成网络序列
// a. 字符串风格的点分十进制的IP地址转成 4 字节IP
// b. 主机序列,转成网络序列
// in_addr_t inet_addr(const char *cp) -> 同时完成 a & b
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP
local.sin_addr.s_addr = INADDR_ANY; // htonl(INADDR_ANY);
// 2.1 bind sockfd和网络信息(IP(?) + Port)
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error, %s, %d\n", strerror(errno), errno);
exit(BIND_ERROR);
}
LOG(INFO, "socket bind success\n");
}
2.echo_server主体实现
2.1. 我们要让server先收数据
recvfrom
函数
用于从一个套接字(由 _sockfd
标识)接收数据。
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
一般使用recvfrom函数,从socket文件中获取数据,并可以得到发送者的信息
- sockfd:从指定的socket文件中读取数据
- buf:缓冲区,将数据读取到这里
- len:缓冲区的长度
- src_addr:输出型参数,获取发送者的信息
- addrlen:输出型参数,获取发送者结构体的长度
2.2. 我们要将server收到的数据,发回给对方
sendto函数
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- sockfd: socket文件操作符,绑定了确定的IP地址与端口,保证数据按照该文件绑定的方式进行通信
- buf:指向包含要发送数据的缓冲区的指针。这个缓冲区应该已经填充了您想要发送的数据。
- len:buf指向的缓冲区中数据的长度,以字节为单位。这个值告诉sendto函数要发送多少字节的数据。
- flags:这个参数通常设置为0,表示没有特殊的发送选项。不过,它可以是一些标志的组合,比如 MSG_CONFIRM(用于TCP,确认路径是有效的)或MSG_DONTROUTE(数据不应该通过网关发送)。
- dest_addr:指向sockaddr结构体的指针,该结构体包含了数据将要发送到的目标地址和端口。对于IPv4,这通常是一个sockaddr_in结构体,而对于IPv6,则是一个sockaddr_in6结构体。
- addrlen:dest_addr指向的sockaddr结构体的大小,以字节为单位。这确保了无论在何种平台上,传递给sendto的都是正确的字节大小。
我们一般使用sendto函数来进行发送数据
2.3.代码
cpp
void Start()
{
// 一直运行,直到管理者不想运行了, 服务器都是死循环
// UDP是面向数据报的协议
_isrunning = true;
while (true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化成为sizeof(peer)
// 1. 我们要让server先收数据
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
InetAddr addr(peer);
LOG(DEBUG, "get message from [%s:%d]: %s\n", addr.Ip().c_str(), addr.Port(), buffer);
// 2. 我们要将server收到的数据,发回给对方
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
_isrunning = false;
}
3.client客户端的实现
3.1.创建socket
跟server端一样,第一步首先要上来创建socket,
3.2.填充sockaddr_in结构
这一步骤也是跟server端一样。
3.3.client要不要bind?
一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!
- 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind ---为什么?防止client port冲突。比如抖音和淘宝使用了同一个端口造成冲突!要bind,必然要和port关联!
- 什么时候bind呢?首次发送数据的时候
3.4.直接通信
流程如下:
- 客户端先输入数据,发送到服务端
- 服务端接收数据
- 服务端再将接收到的数据发送给客户端
- 最后客户端在屏幕回显出自己原本发送的数据
3.5.代码
cpp
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
}
// 2. client要不要bind?一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!
// a. 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind ---
//为什么?防止client port冲突。比如抖音和淘宝使用了同一个端口造成冲突!要bind,必然要和port关联!
// b. 什么时候bind呢?首次发送数据的时候
// 构建目标主机的socket信息
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());
std::string message;
// 2. 直接通信即可
while(true)
{
std::cout << "Please Enter# ";
std::getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
4.效果展现
成功!
server端为什么不需要IP端口?
因为server默认绑定的就是0.0.0.0,代表绑定自己的所有网卡信息,所以就不要我们自己手动填写啦。