文章目录
- 一、Socket套接字
- 二、Udp 常见API
- 1. int socket(int domain, int type, int protocol);
- 2. int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- 3. ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);
- 4. ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr,socklen_t dest_len);
- 5. uint16_t htons(uint16_t hostshort);
- 6. in_addr_t inet_addr(const char *cp);
- 三、Server端
- Client端
- Windows Client端
- 简易聊天室运行图
一、Socket套接字
在互联网中,我们在网络层采用 IP + 端口号 的方式即可在全网中找到唯一进程,IP标识唯一主机,端口号标识该主机端口号绑定的进程。
这就是Socket套接字。
二、Udp 常见API
1. int socket(int domain, int type, int protocol);
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
参数 int domin :指定要在其中创建套接字的通信域。这里我们使用AF_INET采用IPV4通信域。
参数 int type : 指定要创建的套接字类型。 Udp协议是采用面向数据流,所以是用SOCK_DGRAM。
参数 int protocol :指定与套接字一起使用的特定协议。
返回值: 如果成功则返回创建的socket 文件描述符, 失败则返回-1.
代码示例
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd == -1) { exit(1); }
2. int bind(int socket, const struct sockaddr *address, socklen_t address_len);
绑定端口号 (TCP/UDP, 服务器)
参数 int socket 就是使用socket接口函数所创建的那个socket文件描述符。
struct sockaddr
由于底层有不同的网络协议,所以它们的地址格式并不相同,所以通常使用struct sockaddr* 作为参数,然后根据前16位地址类型来确定协议内容。
参数 socklen_t address_len, 结构体sockaddr的长度。
typedef unsigned int socklen_t
返回值: 如果绑定成功则返回0, 失败则返回-1.
代码示例
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd == -1) { exit(1); } struct sockaddr_in Server; bzero(&Server, 0); Server.sin_family = AF_INET; Server.sin_addr.s_addr = inet_addr(_ip.c_str()); //? Server.sin_port = htons(_port); //? int n = bind( socket_fd, (const struct sockaddr *)&Server, sizeof(Server)); if (n != 0) { exit(2); }
3. ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);
参数 int socket 就是使用socket接口函数所创建的那个socket文件描述符。
参数 void *restrict buffer为缓冲区,可以用来存放接收的字节数据。
参数 size_t length 为读取的字节长度。
参数 struct sockaddr *restrict address,输出型参数,用于获取发送方的socketaddr。
参数 socklen_t *restrict address_len,输出型参数,用于获取发送方的sockaddr的长度。
示例代码
char inbuffer[1024]; while (true) { struct sockaddr_in client; socklen_t len = sizeof(client); // unsigned int bzero(&client, 0); // 服务器接受数据 ssize_t n = recvfrom(_socket_fd, inbuffer, sizeof inbuffer - 1, 0, (sockaddr *)&client, &len); if (n > 0) { std::string client_ip = inet_ntoa(client.sin_addr); uint16_t client_port = ntohs(client.sin_port); userCheck(client_ip, client); inbuffer[n] = '\0'; if(strcmp(inbuffer,"login\0") == 0) continue; std::string info = func(inbuffer, client_ip, client_port); senMessage(info); } }
4. ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr,socklen_t dest_len);
参数与 recvfrom类似。不同的是,这里的const struct sockaddr *dest_addr和socklen_t dest_len 变成了输入型参数。 因为你要发送消息,就需要知道对方的socket信息。
5. uint16_t htons(uint16_t hostshort);
这个函数的作用是什么呢? 这就涉及到网络字节序
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
规定网络数据在内存中必须采用大端存储方式!!
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
所以这个函数的作用就是将hostshort转换为网络字节序。
6. in_addr_t inet_addr(const char *cp);
由于我们习惯将IP地址 写成这样的格式xxx.xxx.xxx.xxx(例如192.168.0.1),而在网络层面,我们肯定是采用四字节的int来存储IP,所以有这么一个函数将一个xxx.xxx.xxx.xxx的格式的IP转换为四字节int。
三、Server端
1.初始化Socket套接字
代码如下(示例):
// 申请套接字 int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd == -1) { logMessage(FATAL, "Socket Applied Fault"); exit(SOCKET_DENIED); } _socket_fd = socket_fd; logMessage(DEBUG, "Socket Applied Success"); // bind套接字 struct sockaddr_in Server; bzero(&Server, 0); Server.sin_family = AF_INET; Server.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr可以将ip形式x.x.x.x的字符串转化为大段存储的in_addr_t类型(typedef uint32_t in_addr_t;) Server.sin_port = htons(_port); int n = bind(_socket_fd, (const struct sockaddr *)&Server, sizeof(Server)); if (n != 0) { logMessage(FATAL, "Bind Fault"); exit(BIND_FAULT); }
2.发送和接收
代码如下(示例):
void userCheck(std::string &client_ip, struct sockaddr_in &user) { auto it = _user_map.find(client_ip); // std::cout << "开始检测" << std::endl; if (it == _user_map.end()) { //没找到 std::cout << "新用户[" << client_ip << "]上线..." << std::endl; } _user_map[client_ip] = user; } void senMessage(const std::string& info) { for(auto& user:_user_map ) { sendto(_socket_fd, info.c_str(), info.size(), 0, (const struct sockaddr *)&(user.second), sizeof(user.second)); } } std::string func(const char *res, const std::string& client_ip, const uint16_t client_port) { char buffer [1024]; snprintf(buffer, sizeof buffer,"[%s:%d]# %s",client_ip.c_str(),client_port,res); std::string mes = buffer; std::cout << mes << std::endl; return mes; } void run(func_t func) { char inbuffer[1024]; while (true) { struct sockaddr_in client; socklen_t len = sizeof(client); // unsigned int bzero(&client, 0); // 服务器接受数据 ssize_t n = recvfrom(_socket_fd, inbuffer, sizeof inbuffer - 1, 0, (sockaddr *)&client, &len); if (n > 0) { std::string client_ip = inet_ntoa(client.sin_addr); uint16_t client_port = ntohs(client.sin_port); userCheck(client_ip, client); inbuffer[n] = '\0'; if(strcmp(inbuffer,"login\0") == 0) continue; std::string info = func(inbuffer, client_ip, client_port); senMessage(info); } } }
Server端全代码
//UdpServer.hpp #pragma once #include <iostream> #include <string> #include <sys/socket.h> #include <cstdlib> #include <unistd.h> #include "log.hpp" #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <functional> #include <unordered_map> const std::string default_ip = "0.0.0.0"; //? typedef std::function<std::string(const char*, const std::string &, const uint16_t)> func_t; enum { SOCKET_DENIED = 1, BIND_FAULT }; class UdpServer { public: UdpServer(const std::string &ip = default_ip, const uint16_t port = 8080) : _socket_fd(0), _ip(ip), _port(port) { } void init() { // 申请套接字 int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd == -1) { logMessage(FATAL, "Socket Applied Fault"); exit(SOCKET_DENIED); } _socket_fd = socket_fd; logMessage(DEBUG, "Socket Applied Success"); // bind套接字 struct sockaddr_in Server; bzero(&Server, 0); Server.sin_family = AF_INET; Server.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr可以将ip形式x.x.x.x的字符串转化为大段存储的in_addr_t类型(typedef uint32_t in_addr_t;) Server.sin_port = htons(_port); int n = bind(_socket_fd, (const struct sockaddr *)&Server, sizeof(Server)); if (n != 0) { logMessage(FATAL, "Bind Fault"); exit(BIND_FAULT); } } void userCheck(std::string &client_ip, struct sockaddr_in &user) { auto it = _user_map.find(client_ip); // std::cout << "开始检测" << std::endl; // std::cout << (*it).first << std::endl; if (it == _user_map.end()) { //没找到 std::cout << "新用户[" << client_ip << "]上线..." << std::endl; } _user_map[client_ip] = user; } void senMessage(const std::string& info) { for(auto& user:_user_map ) { sendto(_socket_fd, info.c_str(), info.size(), 0, (const struct sockaddr *)&(user.second), sizeof(user.second)); // std::cout << "已发送数据给" << user.first << std::endl; } } void run(func_t func) { char inbuffer[1024]; while (true) { struct sockaddr_in client; socklen_t len = sizeof(client); // unsigned int bzero(&client, 0); // 服务器接受数据 ssize_t n = recvfrom(_socket_fd, inbuffer, sizeof inbuffer - 1, 0, (sockaddr *)&client, &len); if (n > 0) { std::string client_ip = inet_ntoa(client.sin_addr); uint16_t client_port = ntohs(client.sin_port); // std::cout << client_ip << " : " << client_port <<std::endl; userCheck(client_ip, client); inbuffer[n] = '\0'; if(strcmp(inbuffer,"login\0") == 0) continue; // std::cout << "Server get message# " << inbuffer << std::endl; std::string info = func(inbuffer, client_ip, client_port); senMessage(info); // sendto(_socket_fd, info.c_str(), info.size(), 0, (const struct sockaddr *)&client, len); // sendto(_socket_fd, inbuffer, sizeof inbuffer, 0, (const struct sockaddr *)&client, len); } } } ~UdpServer() { if (_socket_fd == 0) { close(_socket_fd); } } private: int _socket_fd; std::string _ip; uint16_t _port; std::unordered_map<std::string, struct sockaddr_in> _user_map; };
//main.cc #include "Udpserver.hpp" std::string func(const char *res, const std::string& client_ip, const uint16_t client_port) { //std::cout << "[" << client_ip << " : " << client_port << "] # " << res << std::endl; char buffer [1024]; snprintf(buffer, sizeof buffer,"[%s:%d]# %s",client_ip.c_str(),client_port,res); std::string mes = buffer; std::cout << mes << std::endl; return mes; } int main() { UdpServer us; us.init(); us.run(func); return 0; }
const std::string default_ip = “0.0.0.0”;
服务器将ip绑定为0.0.0.0是什么意思呢?
指定ip为本机的任意ip. 尤其适用于本机有多个网卡的情况下,可根据目的地址的默认路由选择合适的网卡建立链路,收发数据。
Client端
#define terminal "/dev/pts" void Usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl; } enum { SOCKET_DENIED = 1, }; struct Thread_Data { int socket_fd; struct sockaddr_in server; }; void *recv_mes(void *args) { Thread_Data* data = (Thread_Data*)args; char buffer[1024]; bzero(buffer,0); while (true) { struct sockaddr_in tmp; socklen_t tmp_len; int n = recvfrom(data->socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &tmp_len); if (n > 0) { buffer[n] = 0; std::cerr << buffer << std::endl; } bzero(buffer,0); } } void *send_mes(void *args) { Thread_Data* data = (Thread_Data*)args; std::string message = "login"; sendto(data->socket_fd, message.c_str(), message.size(), 0, (const struct sockaddr *)&data->server, sizeof(data->server)); while (true) { std::cout << "Please Enter Message@ "; std::getline(std::cin, message); sendto(data->socket_fd, message.c_str(), message.size(), 0, (const struct sockaddr *)&data->server, sizeof(data->server)); } } int main(int args, char *argv[]) { if (args != 3) { Usage("./UdpClient"); } int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd == -1) { logMessage(FATAL, "Socket Applied Fault"); exit(SOCKET_DENIED); } struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr(argv[1]); server.sin_port = htons(std::atoi(argv[2])); pthread_t recv_thread, send_thread; Thread_Data data; data.server = server; data.socket_fd = socket_fd; pthread_create(&send_thread, nullptr, send_mes, (void *)&data); pthread_create(&recv_thread, nullptr, recv_mes, (void *)&data); pthread_join(recv_thread, nullptr); pthread_join(send_thread, nullptr); return 0; }
采用了多线程的方式, 这里需要知道,因为socket本就支持同时可读可写,我们可以理解为它就是线程安全的。
Windows Client端
#include<iostream> #include<string> #include<WinSock2.h> #include<Windows.h> #include<thread> #include<functional> #pragma comment(lib, "ws2_32.lib") // 链接库文件 #pragma warning(disable:4996) //防止VS发出4996号警告 const int server_port = 8080; const std::string server_ip = ""; //提前写好服务器IP //warning : 多线程版本仍有一些小问题 struct Thread_Data { SOCKET socket_fd; struct sockaddr_in server; }; void recv_mes(const Thread_Data& data) { char buffer[1024]; while (true) { memset(buffer, 0, sizeof(buffer)); struct sockaddr_in tmp; int tmp_len = sizeof(tmp); int n = recvfrom(data.socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmp_len); if (n > 0) { buffer[n] = 0; std::cout << buffer << std::endl; } //std::cout << n << " errno:" << errno << std::endl; } } void send_mes(const Thread_Data& data) { std::string message; std::cout << "Send A Message To Start A Chat "; while (true) { std::getline(std::cin, message); sendto(data.socket_fd, message.c_str(), (int)message.size(), 0, (const struct sockaddr*)&data.server, sizeof(data.server)); } } int main() { //初始化网络环境 WSADATA wsd; WSAStartup(MAKEWORD(2, 2), &wsd); system("chcp 65001"); SOCKET socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (socket_fd == SOCKET_ERROR) { perror("Socket Error"); exit(1); } struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr(server_ip.c_str()); server.sin_port = htons(server_port); std::thread threads[2]; Thread_Data data; data.server = server; data.socket_fd = socket_fd; sendto(socket_fd, "login", 5, 0, (const struct sockaddr*)&server, sizeof(server)); threads[0] = std::thread(recv_mes, std::ref(data)); threads[1] = std::thread(send_mes, std::ref(data)); threads[0].join(); threads[1].join(); closesocket(socket_fd); WSACleanup(); //清理网络环境 return 0; }
这里的Windows端也是实现了一个多线程的版本,因为是不同的操作系统,在接口上是有一些差别的,不过大部分都差不多。