网络实践——Socket编程UDP
文章目录
- Socket编程UDP
- UDP接口的使用铺垫
- socket
- recvform && sendto
- bind
- 字节序转化使用(Tips)
- 实践部分
- version_1@echo_server
- version_2@dict_server
- version_3@chat_server
Socket编程UDP
在了解了相关的网络基础知识后,我们不会像学系统知识一样,先学原理,再讲应用。
我们先先不选择学习网络原理。我们先来试着学习一下使用接口来完成一些简单地实践。
在这个部分中,我们不只使用网络相关知识,而是结合前面系统部分学习时,完成的一些组件代码来使用!这些我们后面会见到的!
UDP接口的使用铺垫
先说一下,有了网络基础知识 + 系统部分的知识。其实我们只需要了解一下UDP相关接口的使用,其实就能较为熟练地学习如何使用。下面,我们将把网络中将用的接口进行简单讲解。
socket
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);
这个文件,是用来创建套接字的!我们在网络基础部分讲过,两个进程的通信是基于套接字(端口号 + ip)的!具体的创建,就是用这个接口。
第一个参数domain是选择使用什么域来进行通信:
这里我们记住是选择AF_INET进行网络通信即可!
第二个参数type是选择使用什么方式传输,比如TCP/UDP:
字符流 -> SOCK_STREAM -> TCP
数据包 -> SOCK_DGRAM -> UDP
第三个参数protocol是要我们选择协议,默认给0就是TCP/IP了,记住即可!
返回值:
成功,返回一个file descriptor,即文件描述符!即使我们不知道这个网络通信的原理,但是我们至少可以直到,当前进程的文件描述符表定会有一个位置指向socket文件!
所以,我们就把它当成特殊的网络文件来使用!这符合 Linux下一切皆文件!
recvform && sendto
我们先来看着两个函数的相关信息,它们具有较强相似性:
recvform:
NAMErecv, recvfrom, recvmsg - receive a message from a socketSYNOPSIS#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);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.
sendto:
NAMEsend, sendto, sendmsg - send a message on a socketSYNOPSIS#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);RETURN VALUEOn success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set appropriately.
见名思意:
recvform是从套接字文件中获取内容;sendto是向套接字文件中发送内容。
参数解释:
-
int sockfd,就是对应的套接字文件!这个不需要解释。
-
const void *buf,这个就是一个缓冲区,recvfrom把从套接字文件中读到的内容读取到该缓冲区。sendto将该缓冲区内的内容发送到套接字文件。
-
size_t len,即缓冲区长度。
-
int flags,这个是选择是否阻塞读取/发送的。默认给0就是阻塞。也就是说,recvfrom读不到内容就会阻塞,sendto没有内容发送也会阻塞。这个我们在系统那里也见过。
-
struct sockaddr,这个其实我们早在网络基础部分的时候,就已经讲到过这个。我们说了,设计socket网络通信的时候,为了能够让网络通信和本地通信公用一套接口,所以设计了这么个c语言版的基类!
所以,如果需要收发消息,其实对方进程的相关信息:ip、端口号、通信协议,都会在对应的结构体上体现出来。所以,未来在使用socket通信的时候,如何知道或者发送自己的相关信息,就是靠着这个结构体sockaddr_in或者sockaddr_un,强转类型为sockaddr对应的地址变量后,然后进行相关操作!
6.socklen_t *addrlen和socklen_t addrlen:
6.1. 对于recvfrom收消息来说,参数是socklen_t *addrlen,这很明显是一个指针地址变量!所以,是需要我们把sockaddr_in的地址传进去给第五个参数,然后需要定义一个变量指明该结构体大小,传入地址给第六个参数。
Tips:因为该变量是输出型参数,实际上它会返回真正读到的字节数(因为网络传输可能丢包)
6.2. 对于sendto发消息来说,这个就没什么好说的了,本身数据就在,直接传对应的大小即可
bind
NAMEbind - bind a name to a socketSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>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.
没看错,这里还有一个接口叫做bind,但是,这个不是c++11后提出的std::bind!
这个接口最重要的就是,将套接字文件和对应的进程的IP地址和端口号(在结构体sockaddr_in内存储了),绑定到对应的套接字文件中!
bind的作用:
显式绑定:bind()将套接字固定到特定IP+端口,成为该地址的唯一接收者。
未绑定时:系统在首次发送数据时自动分配随机端口(IP默认为所有本地接口)
这里理解一下就好,实在看不懂的话等后续讲解的时候再说一遍如何使用。现在只需要学会使用即可,原理等后期再来讲解!
字节序转化使用(Tips)
这个在网络基础中也是提到了。这里再多提醒一下,因为后续的实践中,必然是有从网络序列转主机序列的内容,也有主机序列转网络序列的!所以,这些是必须使用的!
实践部分
version_1@echo_server
源码:version_1@echo_server
第一个版本,我们希望实现一种功能:
即有一个服务器,然后其余所有的进程都可以给其发消息,然后服务器处理后再转发给对应的进程,这样子就起到了一个回显服务器的效果!
所有的过程都已经在代码的注释中展现出来了。
version_2@dict_server
源码:version_2@dict_server
第二个版本,其实就是进行一次解耦合!让第一个版本中对于信息回显的处理模块,转化为服务器调用词典翻译模块!其实主要的逻辑和第一个版本差别不大。
只不过是引入多了一个模块,让词典从对应的配置文件中读取对应信息,然后服务器进行调用后再进行转发结果给请求该服务的进程!
version_3@chat_server
源码:version_3@chat_server
第三个版本我们来详细说明一下:
第三个版本我们希望做到一个群聊转发功能,即服务器在接收到某个客户端发送来的消息后,要转发给所有处于在线的用户。
1.我们这里规定:
默认第一次发送消息的就是要加入群聊的。服务器接收到消息之后,要怎么样才能转发给所有的在线用户呢?-> 添加一层路由层,用于管理在线用户(组织描述)和消息转发!
2.但是,我们觉得效率太低了,所以希望的是,服务器将收到的信息推给后端线程池,让线程池自行调用路由转发功能!所以引入了线程池。
3.此前版本1、2写的客户端使用代码是有问题的!因为强行规定了先发消息才能收消息。所以,在实现群聊过程中,发现客户端只有发了消息,才能接收到其他客户端被转发的消息。所以,为此我们进行了处理,就是让客户端多线程处理!即创建两个线程,同时进行收发!
4.线程池访问路由表的时候(就是底层的一个哈希),也是会涉及到线程安全的。但是,因为今天的实现并没有规定消息的协议(是否退出、私法、群发、还是请求服务器处理…)。我们仅仅只是把收到的内容当字符串处理!
但是不管怎么说,线程池内每个线程访问的时候,是会出现数据不一致的问题的!因为STL不是线程安全的,所以我们这里就粗暴一点,直接加锁!
5.实践的时候,因为我没有多台Linux机器,所以,使用了Windows进行辅助测试,测试是否能够跨网通信!代码如下:
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = ""; // 填写你的云服务器ip
uint16_t serverport = ; // 填写你的云服务开放的端口号SOCKET sockfd;
struct sockaddr_in server;void recv_msg() {while (1) {char buffer[1024];struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}
}void send_msg() {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 (1) {std::string message; std::getline(std::cin, message);if (message.empty()) continue; sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));}
}int main() {WSADATA wsd; WSAStartup(MAKEWORD(2, 2), &wsd); sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::thread Recv(recv_msg);std::thread Send(send_msg);Recv.join();Send.join();closesocket(sockfd);WSACleanup();return 0;
}//int main()
//{
// WSADATA wsd;
// WSAStartup(MAKEWORD(2, 2), &wsd);
//
//
// 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());
//
// sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// if (sockfd == SOCKET_ERROR)
// {
// std::cout << "socker error" << std::endl;
// return 1;
// }
//
//
//
// std::string message;
// char buffer[1024];
// while (true)
// {
// std::cout << "please input: ";
// std::getline(std::cin, message);
// if (message.empty()) continue;
// sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));
// struct sockaddr_in temp;
// int len = sizeof(temp);
// int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
// if (s > 0)
// {
// buffer[s] = 0;
// std::cout << buffer << std::endl;
// }
// }
//
// closesocket(sockfd);
// WSACleanup();
// return 0;
//}
前面一份是用于版本3的测试,后面是用于版本1、2的测试!这里可以搜一下相关大模型了解一下用法,其实用法和Linux下的基本一致。