目录
- 1. 常见API
- 1.1 sockaddr 结构
- 2. udp_socket
- 2.1 了解相关接口
- 2.2 基础UDP通信
1. 常见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);
1.1 sockaddr 结构
未来在使用 socket 套接字编程时,一般默认是需要把本主机的IP地址与端口号通过系统调用接口进行绑定,其中的网络套接字就有不同的种类:
- 域间套接字编程(用户一个主机内的多个进程间通信,即本地通信)
- 原始套接字编程(绕过传输层,直接使用网络层、链路层的接口进行编码,通过用于编写网络工具:网络状态检测、网络抓包等)
- 网络套接字编程(使用传输层进行用户通信)
由于网络通信场景的不同,因此引出了不同种类的套接字,理论上不同的套接字种类就需要不同的接口,但在网络接口的设计上,把网络接口全部统一抽象化,而网络接口统一的前提是,每一个接口的参数的类型必须是统一的。
实际上,我们在网络通信(网络套接字编程) 时使用的是 struct sockaddr_in 这个结构体,里面包含16位端口号、32位 IP 地址、8字节的填充字段,而在域间通信时使用的则是struct sockaddr _un,里面则只需要包含本机通信的两个进程看到同一份资源的路径,不同种类的网络套接字背后的数据类型是不同的。这两个结构体是作为编码时使用的数据类型,而在实际网络接口设计中,设计的是 struct sockaddr 这个接口,无论是网络通信 or 本地通信,前面都有 2 字节数据,表明通信的类型, struct sockaddr 同样也有这 2 个字节的数据,因此将来在使用网络接口时,传递的都是 struct sockaddr 结构体,然后在每个接口内部中都会实现分流,类似于:
if(address->type == AF_INEF) { 网络通信 }
else { 本地通信 }
这样一来,无论背后使用的是哪种通信(使用的是 struct sockaddr_in 或者 struct sockaddr _un),上层在调用网络接口时都不关心,在上层调用时统一使用 struct sockaddr,这就是所谓的 “将网络接口全部统一抽象化”,具体在后面编码实现时体现。
2. udp_socket
2.1 了解相关接口
NAMEsocket - create an endpoint for communication // 创建套接字SYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);RETURN VALUE成功时返回文件描述符。失败返回 -1,errno 被设置。后续的一切套接字的操作都需要通过返回的 socket 完成。因此创建一个套接字的本质就是打开一个文件,struct file 指向网卡设备On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.参数:
domain:套接字的通信域(或协议族)(网络通信 or 本地通信),常见的包括:AF_INET:IPv4 地址族AF_INET6:IPv6 地址族AF_UNIX:本地通信(Unix 域套接字)AF_ROUTE:路由套接字
type:套接字的类型:SOCK_STREAM:面向流的套接字,提供可靠的、双向的字节流。SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的数据报通信。SOCK_RAW:原始套接字,允许直接访问底层协议(一般用于网络协议的研究和开发)。
protocol:指定特定的协议,默认设置为0,即系统自动选择与所选类型和域匹配的默认协议,也可指定特定协议,例如:对于 SOCK_STREAM,可以选择 IPPROTO_TCP(TCP 协议)。对于 SOCK_DGRAM,可以选择 IPPROTO_UDP(UDP 协议)。
// struct sockaddr_in 底层源码(部分)
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family // ## 合并两边的符合/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};/* Structure describing an Internet socket address. */
struct sockaddr_in
{__SOCKADDR_COMMON (sin_); /* 底层宏替换##合并后即:sin_family */in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];
};
NAMEbind - bind a name to a socket // 绑定套接字int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);RETURN VALUEOn success, zero is returned. On error, -1 is returned, and errno is set appropriately.参数:
sockfd:创建的套接字的返回值
addr:上述介绍的 sockaddr_in (强转即可)
addrlen:addr 的大小
NAMErecv, recvfrom, recvmsg - receive a message from a socket // 接收数据报ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);RETURN VALUEThese calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. The return value will be 0 when the peer has performed an orderly shutdown.参数:
sockfd:创建的套接字的返回值
buf:用于接收数据的缓冲区
len:缓冲区的大小
flags:指定接收数据的方式,默认设置为0,表阻塞接收MSG_WAITALL:在接收到所有请求的字节数之前不会返回,这使得函数在接收过程中会阻塞。MSG_PEEK:查看接收队列中的消息,但不从队列中移除任何消息,这样后续的接收操作仍然可以接收到这些消息。MSG_DONTWAIT:非阻塞模式;如果没有数据可接收,则立即返回,而不是阻塞等待。
addr:输出型参数,发送端的地址信息(如果接收到数据需要返回,必须知道对方的IP、Port等信息)
addrlen:addr 的大小
NAMEsend, sendto, sendmsg - send a message on a socketssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);RETURN VALUEOn success, these calls return the number of characters sent. On error, -1 is returned, and errno is set appropriately.
NAMEpopen, pclose - pipe stream to or from a processSYNOPSIS#include <stdio.h>// 底层调用 frok 创建子进程,并通过管道将指定命令传递给子进程执行,子进程的退出状态可通过pclose获取FILE *popen(const char *command, const char *type);参数:
command:指定要执行的命令
type:指定打开管道的方式
-
地址转换函数
本篇文章只介绍基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示32位的 IP 地址,但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换#include <arpa/inet.h>// 字符串 ==> in_addrint inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);int inet_pton(int af, const char *src, void *dst);// in_addr ==> 字符串char *inet_ntoa(struct in_addr in);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 的结果),那么是否需要调用者手动释放呢?
The inet_ntoa() function converts the Internet host address in, given in network byte order, to a string in IPv4 dotted-decimal notation. The string is returned in a statically allocated buffer, which subsequent calls will overwrite.
man 手册对此进行了说明:inet_ntoa 函数是把转换后的 ip 字符串放到了静态存储区,这个时候不需要我们手动进行释放。那么问题来了,inet_ntoa把 结果放到自己内部的一个静态存储区,这样导致第二次调用时的结果会覆盖上一次的结果。
在APUE中,明确提出 inet_ntoa 不是线程安全的函数,但是在 centos7 上测试并没有出现问题,因此可能是内部的实现加了互斥锁。因此后续推荐使用 inet_ntop,由用户自己提供缓冲区维护返回结果,来规避线程安全的隐患。
-
-
关于 IP 的一个问题
云服务器禁止绑定公网 ip。因为有可能一些机器不只有一个网卡设备,那么就会配置多个 IP 地址。因此,这种配置的服务器下,如果只绑定了一个IP,那么数据向上交付时,只能收到发送给绑定IP的数据,数据发送给另外其它没有绑定的IP,这台服务器也收不到。所以,一般 bind 绑定 IP 地址时绑定的是 0,即凡是发给这台主机的数据,不管发送数据时绑定的是哪个IP,只要是这个主机的IP,都要根据端口号向上交付。 -
关于 Port 的一个问题:
[0, 1023] 为系统内定的端口号,一般都要有固定的应用层协议使用,例如 http: 80、https: 443 等。
2.2 基础UDP通信
服务端承担数据的接收与转发,接收到的每条来自客户端的信息转发到已经启动的每一个客户端上。
客户端实现了多线程版本,将接收与发送分流执行。
// UdpServer.hpp#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"#define DEFAULT_PORT 8888
#define DEFAULT_IP "0.0.0.0"using func_t = std::function<std::string(const std::string&)>;enum{SOCKET_ERR = 1,BIND_ERR
};Log log;
const int size = 1024;class UdpServer
{
public:UdpServer(const uint16_t& port = DEFAULT_PORT, const std::string& ip = DEFAULT_IP): _sockfd(0), _port(port), _ip(ip), _isRunning(false){}void Init(){// 1. 创建 upd_socket// 2. Udp 的 socket 是全双工的,允许被同时读写的_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){log(Fatal, "socket create error, sockfd: %d", _sockfd);exit(SOCKET_ERR);exit(SOCKET_ERR);}log(Info, "socket create success, sockfd: %d", _sockfd);// 2. 绑定struct sockaddr_in local;bzero(&local, sizeof(local)); // 将指定空间全部初始化为0local.sin_family = AF_INET; // 套接字结构体的前两个字节需要表明结构体的类型,也即通信的协议族:IPv4网络通信local.sin_port = htons(_port); // 因为端口号是要通过网络发送给对方的,所以需要保证端口号是网络字节序// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->uint32_t 2. uint32_t 必须为网络字节序local.sin_addr.s_addr = INADDR_ANY; int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local)); // 网络接口统一设计的体现if(n == -1) {log(Fatal, "bind error, errno: %d, err string: %s\n", errno, strerror(errno));exit(BIND_ERR);}log(Info, "socket bind success, errno: %d, err string: %s\n", errno, strerror(errno));}void CheckUser(const struct sockaddr_in client, const std::string clientip, uint16_t clientport){auto iter = _online_user.find(clientip);if(iter == _online_user.end()){_online_user[clientip] = client;std::cout << "[" << clientip << ": " << clientport << "] add to online user." << std::endl;}} void Broadcast(const std::string& info, const std::string clientip, uint16_t clientport){for(const auto& user : _online_user){std::string message = "[" + clientip + ": " + std::to_string(clientport) + "]# " + info;sendto(_sockfd, message.c_str(), message.size(), 0, (const sockaddr*)(&user.second), (socklen_t)sizeof(user.second));}}// void Run(func_t func)void Run(){ _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);if(n < 0){log(Warning, "recvfrom error, errno: %d, err string: %s\n", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr); // 网络字节序 --> 本地CheckUser(client, clientip, clientport);inbuffer[n] = 0;std::string info = inbuffer;Broadcast(info, clientip, clientport);}}~UdpServer(){if(_sockfd > 0) close(_sockfd);}private:int _sockfd; // 网络文件描述符std::string _ip;uint16_t _port;bool _isRunning;std::unordered_map<std::string, struct sockaddr_in> _online_user;
};
// UdpClient.cc
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;#include "Terminal.hpp"void Usage(std::string proc)
{cout << "Usage: " << proc << " serverip serverport\n" << endl;
}struct ThreadData
{struct sockaddr_in server;int sockfd;std::string serverip;
};void* recv_message(void* args)
{// OpenTerminal(); // 重定向终端ThreadData* td = static_cast<ThreadData*>(args);socklen_t len = sizeof(td->server);char buffer[4096];while (true){struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;cerr << buffer << endl;}}}void* send_message(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);socklen_t len = sizeof(td->server);string message;std::string welcome = "[" + td->serverip + "] is comming... ";sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (struct sockaddr*)&td->server, len);while(true){std::cout << "Please Enter@ ";getline(cin, message);// cout << message << endl;sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&td->server, len);}
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct ThreadData td;bzero(&td.server, sizeof(td.server));td.serverip = serverip;td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(td.server);td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(td.sockfd < 0){cout << "socket error" << endl;return 1;}// client 也需要 bind,因为 client 也有 ip、port,用于服务器返回的数据// 只不过 client 不需要用户显式绑定,一般由OS自由随机选择// 如果让用户自己绑定端口号,A进程绑定的是1234,B进程也是1234,那么哪个应用先启动哪个应用就能够正常使用,而后启动的进程就无法使用该端口了// 而不同企业的客户端也不可能协调使用客户端的端口号,因此客户端的端口号由OS自己选择最佳// 一个进程是可以绑定多个端口号的,如果让用户自己绑定,那么不妨有恶意进程绑定多个端口号,从而让其他进程无法使用// 对于服务器而言,端口号必须由用户自己绑定,则是因为服务器需要监听客户端的请求,请求都是由客户端向服务器发送的,服务器可从来不会主动发送给客户端// 而如果让服务器随机绑定端口号,那么可能每次启动时绑定的端口号都不一样,客户端下一次可能就无法请求服务器了。// 系统是在首次发送数据时,给客户端绑定端口号的。pthread_t receiver, sender;pthread_create(&receiver, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(receiver, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}
// Main.cc#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024-65535]\n" << std::endl;
}std::string handler(const std::string& str, const std::string clientip, uint16_t clientport)
{std::cout << "[" << clientip << ": " << clientport << "]# " << str;std::string res = "Server get a message: " + str;std::cout << res << std::endl;return res;
}bool SafeCheck(const std::string& cmd)
{std::vector<std::string> key_words = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while","touch","for"};for(auto& word : key_words) {auto pos = cmd.find(word);if(pos != std::string::npos) return false;}return true;
}std::string ExcuteCommand(const std::string& cmd)
{std::cout << "get a request cmd: " << cmd << std::endl;if(!SafeCheck(cmd)) return "You are a bad-man!\n";FILE* fp = popen(cmd.c_str(), "r");if(fp == nullptr){perror("popen");return "error";}std::string ret;char buffer[4096];while(true){char* ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;ret += buffer;}pclose(fp);return ret;
}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();return 0;
}
可以通过不同的终端,将客户端发送与接收的消息分离开,可以通过对输出流做重定向来完成。
# 示例./udpclient serverip serverport 2>/dev/pts/0
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!