3.3 客户端初始化
3.3.1 客户端创建套接字
将客户端封装成一个类,当定义出一个客户端对象后也需要对其进行初始化,而客户端在初始化时也要创建套接字,之后客户端发送数据或接收数据也就是对这个套接字进行操作
客户端创建套接字时选择的协议家族是AF_INET,需要的服务类型是SOCK_DGARM,当客户端被析构时可以选择关闭对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接即可,而不需进行绑定操作
class UdpClient { public: UdpClient(string server_ip,uint16_t server_port):_server_ip(server_ip),_server_port(server_port) {} bool InitClient(); ~UdpClient() {} private: int _socket_fd; string _server_ip; uint16_t _server_port; }; bool UdpClient::InitClient() { _socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (_socket_fd < 0){ std::cerr << "socket create error" << std::endl; return false; } return true; } UdpClient::~UdpClient() { if(_socket_fd < 0) close(_socket_fd); }
3.3.2 客户端的绑定问题
由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要进行端口号的绑定,而客户端不需要
因为服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,IP地址一般对应的就是域名,而端口号一般没有显示指明过,因此服务端的端口号一定要是一个众所周知的端口号,并且选定后不能轻易改变,否则客户端是无法知道服务端的端口号的,这就是服务端要进行绑定的原因,只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口
客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要和特定客户端进程强相关
若客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且若这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了。因此客户端端口可以动态的进行设置,客户端的端口号就不需要程序员设置,当调用类似于sendto()这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号
客户端每次启动时使用的端口号可能是变化的,此时只要端口号没有耗尽,客户端就可以正常启动
3.4 启动运行客户端
当客户端初始化完毕后就可以将客户端运行起来,由于客户端和服务端在功能上是相互补充的,既然服务器是在读取客户端发来的数据,那么客户端就应该向服务端发送数据
sendto函数
发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件中
buf:待写入数据的存放位置
len:期望写入数据的字节数
flags:写入的方式。一般设置为0,表示阻塞写入
dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
addrlen:传入dest_addr结构体的长度
返回值:写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等
由于sendto()提供的参数是 struct sockaddr* 类型的,在传参时需将struct sockaddr*类型强转
提供启动客户端接口
客户端要发送数据给服务端,可以让客户端获取用户输入,不断将用户输入的数据发送给服务端
客户端中存储的服务端的端口号此时是主机序列,需要调用htons()函数将其转为网络序列后再设置进struct sockaddr_in结构体。客户端中存储的服务端的IP地址是字符串IP,需要通过调用inet_addr()函数将其转为整数IP(同时转成网络序列)后再设置进struct sockaddr_in结构体
void UdpClient::Start() { string message; struct sockaddr_in receive; memset(&receive, 0, sizeof(receive)); receive.sin_port = htons(_server_port); receive.sin_family = AF_INET; receive.sin_addr.s_addr = inet_addr(_server_ip.c_str()); while(true) { cout << "please Enter#"; getline(cin, message); sendto(_socket_fd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr*)&receive, sizeof(receive)); } }
引入命令行参数
引入命令行参数,运行客户端时直接在后面跟上对应服务端的IP地址和端口号即可
int main(int argc, char* argv[]) { if(argc < 3) { cerr << "Usage " << argv[0] << " ip port " << endl; return 1; } string serve_ip = argv[1]; uint16_t serve_port = atoi(argv[2]); UdpClient* client = new UdpClient(serve_ip, serve_port); client->InitClient(); client->Start(); return 0; }
3.5 本地测试
服务端和客户端的代码都已经编写完成,可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回,服务端的端口号就是8081
使用netstat命令查看网络信息,可以看到服务端的端口是8081,客户端的端口是36577,客户端也已动态绑定成功了
3.6 INADDR_ANY
进行网络测试,直接让服务端绑定公网IP,此时这个服务端就可以被外网访问
将服务端设置的本地环回改为博主的公网IP,此时重新运行服务端的时候会发现服务端绑定失败
由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,若需要让外网访问,此时需要bind 0。系统当中提供的一个INADDR_ANY(宏值),对应的值就是0
若需要让外网访问,那么进行绑定时就应该绑定INADDR_ANY,此时服务器才能够被外网访问
绑定INADDR_ANY的好处
当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为8081的服务只有一个。这台服务器在接收数据时,多张网卡在底层实际都收到了数据,可能这些数据也都想访问端口号为8081的服务
此时若服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。而若服务端绑定的是INADDR_ANY,那么只要是发送给端口号为8081的服务的数据,系统都会可以将数据自底向上交给该服务端
因此服务端绑定INADDR_ANY这种方案是强烈推荐的,所有的服务器具体在操作时采用的也是这种方案
若你既想让外网访问你的服务器,但又想指向绑定某一个IP地址,那么就不能使用云服务器,此时可以选择使用虚拟机或者自定义安装的Linux操作系统,那个IP地址就是支持自定义绑定的,而云服务器是不支持的
更改代码
bool UdpServer::InitServer() { //创建套接字 _socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if(_socket_fd < 0) { cerr << "socket fail" << endl; return false; } cout << "socket create success , fd:" << _socket_fd << endl; //填充网络通信相关信息 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; //绑定 if(bind(_socket_fd, (struct sockaddr*)&local, sizeof(local)) < 0) { cerr << "bind fail" << endl; return false; } cout << "bind success" << endl; return true; }
此时再重新编译运行服务器时就不会绑定失败了,并且此时再用netstat命令查看时会发现,该服务器的本地IP地址变成0.0.0.0,这意味着该UDP服务器可以在本地读取任何一张网卡里面的数据
3.7 回声功能
由于在进行网络测试的时候,当客户端发送数据给服务端时,服务端会将从客户端收到的数据进行打印,因此服务端是能够看到现象的。但客户端一直在向服务端发送数据,在客户端这边看不出服务端是否收到了自己发送的数据
服务端代码编写
可以将该服务器改成一个回声服务器。当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用sendto函数将收到的数据重新发送给对应的客户端
服务端在调用sendto函数时需要传入客户端的网络属性信息,但服务端是知道客户端的网络属性信息的,因为服务端在此之前就已经通过recvfrom函数获取到了客户端的网络属性信息
void UdpServer::Start() { char buffer[SIZE]; while(true) { struct sockaddr_in ping; socklen_t length = sizeof(ping); ssize_t size = recvfrom(_socket_fd, buffer, SIZE - 1, 0, (struct sockaddr*)&ping, &length); if(size > 0) { buffer[size] = '\0'; uint16_t port = ntohs(ping.sin_port); string ip = inet_ntoa(ping.sin_addr); cout << "[" << ip << ":" << port << "]#" << buffer << endl; } else { cerr << "recvfrom fail" << endl; } string echo_message = "server echo:"; echo_message += buffer; sendto(_socket_fd, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&ping, length); } }
客户端代码编写
当客户端发完数据给服务端后,由于服务端还会将该数据重新发给客户端,因此客户端发完数据后还需要调recvfrom来读取服务端发来的响应数据,客户端接收到服务端的响应数据后,将数据原封不动的打印出来就行了
此时客户端发送给服务端的数据,除了在服务端会打印显示以外,服务端还会将数据再重新发回给客户端,此时客户端也会接收到响应数据然后将该数据进行打印
void UdpClient::Start() { string message; struct sockaddr_in receive; memset(&receive, 0, sizeof(receive)); receive.sin_port = htons(_server_port); receive.sin_family = AF_INET; receive.sin_addr.s_addr = inet_addr(_server_ip.c_str()); while(true) { cout << "please Enter#"; getline(cin, message); sendto(_socket_fd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr*)&receive, sizeof(receive)); char buffer[SIZE]; struct sockaddr_in tmp; socklen_t length = sizeof(tmp); ssize_t size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &length); if (size > 0) { buffer[size] = '\0'; cout << buffer << endl; } } }
3.8 网络测试
此时可以使用 sz命令 将该客户端可执行程序下载到本地机器,然后将该程序发送给你的朋友
当你的朋友收到这个客户端的可执行程序后,可以使用 rz命令 或拖拽的方式将这个可执行程序上传到他的云服务器上,然后使用 chmod命令 给该文件加上可执行权限
先将服务端启动,然后你的朋友将你的IP地址和端口号作为命令行参数运行客户端,就可以访问你的服务器了