C++socket网络编程——udp服务器
目录
一.端口号 VS PID
pid已经能够标识一台主机上的一个唯一一个进程了,为什么还需要端口号?
- 不是所有的进程都需要网络通信,但是所有的进程都需要都pid;
- 系统和网络功能解耦。
另外,一个进程可以绑定多个端口,但一个端口只能被一个进程绑定。
系统内定的端口号【0,1023】一般都要有固定的应用层协议使用,如http:80,https:443。
二.套接字编程的类型
- 域间套接字编程——同一个机器内
- 原始套接字编程——网络工具
- 网络套接字编程——用户间的网络通信
不同的套接字编程类型的接口需要是相同的。

如同用一个统一的sockaddr作为基类,sockaddr_in和sockaddr_un作为子类。使用的时候我们用if语句区分具体是什么类型,如同下面这段代码:
if(address->type == AF_INET) { // 网络套接字 } else if(address->type == AF_UNIX) { // 域间套接字 }为什么操作系统接口设计者在设计的时候不直接使用void*呢,因为那时候C语言还没有void*。
三.socket编程接口
// 创建 socket 文件描述符(TCP/UDP,客户端 + 服务器) int socket(int domain, int type, int protocol); // 绑定端口号(TCP/UDP,服务器) int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 开始监听socket(TCP,服务器) int listen(int sockfd, int backlog); // 接收请求(TCP,服务器) int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 建立连接(TCP,客户端) int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 四.基于udp的服务端和客户端全部代码
客户端
代码如下:
client.cc:
#include <iostream> #include <sys/types.h> #include <sys/socket.h> // #include <stdio.h> #include <unistd.h> #include <strings.h> #include <arpa/inet.h> // sockaddr_in using namespace std; void usage(const std::string& proc) { cout << "Please use like this : " + proc + " [ip] [port]" << endl; } int main(int argc, char* argv[]) { if(argc != 3) { usage(argv[0]); exit(1); } string serverip = argv[1]; uint16_t serverport = stoi(argv[2]); int sockFd = socket(AF_INET, SOCK_DGRAM, 0); if(sockFd < 0) cerr << "sockFd error" << endl; struct sockaddr_in server; socklen_t sz = sizeof(server); bzero(&server, sz); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr(serverip.c_str()); server.sin_port = htons(serverport); std::string temp; while(true) { cout << "请输入消息@ "; getline(cin, temp); int ret = sendto(sockFd, temp.c_str(), temp.size(), 0, (struct sockaddr*)&server, sz); if(ret < 0) std::cerr << "send error" << std::endl; char buffer[1024]; struct sockaddr_in retServer; socklen_t len = sizeof(retServer); int n = recvfrom(sockFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&retServer, &len); if(n > 0) { buffer[n] = 0; cout << "收到消息:" << buffer << endl; } } close(sockFd); return 0; } 服务端
代码如下:
UdpServer.hpp:
#include <string> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <unistd.h> // #include <netinet/in.h> #include <arpa/inet.h> // sockaddr_in #include <iostream> #include <vector> #include <functional> using func_t = std::function<std::string(const std::string&)>; const std::string defaultip = "0.0.0.0"; const uint16_t defaultport = 8080; class UdpServer { public: UdpServer():_sockFd(-1) {} ~UdpServer() { if(_sockFd >= 0) close(_sockFd); } void init(const std::string& ip = defaultip, uint16_t port = defaultport) { _sockFd = socket(AF_INET, SOCK_DGRAM, 0); if(_sockFd < 0) std::cerr << "创建套接字失败" << std::endl; struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_addr.s_addr = inet_addr(ip.c_str()); local.sin_port = htons(port); int ret = bind(_sockFd, (const struct sockaddr*)&local, sizeof(local)); if(ret < 0) std::cerr << "绑定本地套接字信息失败" << std::endl; } void run(func_t func) { while(true) { struct sockaddr_in client; socklen_t sz = sizeof(client); char buffer[1024]; int n = recvfrom(_sockFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&client, &sz); buffer[n] = 0; std::string clientip = inet_ntoa(client.sin_addr); uint16_t clientport = ntohs(client.sin_port); std::cout << "[" << clientip + ":" << clientport << "] send information : " << buffer << std::endl; std::string temp = func(buffer); int ret = sendto(_sockFd, temp.c_str(), temp.size(), 0, (const struct sockaddr*)&client, sz); if(ret < 0) std::cerr << "send error" << std::endl; } } private: int _sockFd; }; main.cc:
#include "UdpServer.hpp" #include <memory> #include <string> #include <string.h> std::string echo(const std::string& cData) { return "server : " + cData; } std::string command(const std::string& cData) { std::string ret; FILE* fp = popen(cData.c_str(), "r"); if(fp == nullptr) { perror("popen"); exit(1); } while(true) { char buffer[1024]; memset(buffer, sizeof(buffer), 0); char* ok = fgets(buffer, sizeof(buffer) - 1, fp); if(ok == nullptr) break; ret += buffer; } pclose(fp); return ret; } int main() { std::unique_ptr<UdpServer> udpServer(new UdpServer()); udpServer->init(); udpServer->run(command); return 0; } makefile:
.PHONY:all all:UdpServer client UdpServer:main.cc g++ -o $@ $^ -std=c++11 client:client.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f UdpServer client五.解释与运行
- 客户端:接收用户输入的字符串(可以是普通消息或系统命令),通过 UDP 发送给指定 IP 和端口的服务端,然后等待并打印服务端的响应。
- 服务端:以类的形式封装(UdpServer),监听指定端口,接收客户端的消息后,通过回调函数处理(默认是执行客户端发送的系统命令并返回执行结果,也可切换为简单回显),再将处理结果通过 UDP 返回给客户端。
运行结果:

这样,我们就可以远程输入命令了,xshell远程连接服务器就是这样类似的原理。
一些细节:

AF_INET指明使用ipv4协议,sock_DGRAM指明面向数据报的udp协议,第2个参数协议类型不用管。

绑定之前将自己的信息填入sockaddr_in这个数据结构中。AF_INET表明是网络socket编程,s_addr表明服务端的ip地址,调用inet _addr函数将它转换成32位数字并且转换为网络字节序列,也就是大端。htons也就是将short类型数据转换为网络字节序列,也就是大端。

倒数第一个参数len 既是输入型参数,也是输出型参数;倒数第二个参数是输出型参数。
网络转成主机的字节序,并且32位数字的ip转换为点分十进制字符串形式。
六.总结
我们还可以进行扩展。将这个服务弄成多人聊天。服务端用一个哈希和客户端的ip地址唯一标识一个客户端,管理起来之后就可以对每一条消息进行广播给每一个客户端了,客户端是多线程的,一个线程负责发送消息,一个线程负责接收消息,为了发送和接收不在一个终端进行,我们可以将接收线程里的标准输出或者标准错误重定向到/dev/pts/某某序号的这个文件夹,用dup2这个函数就可以做到,然后我们客户端就可以在一个终端发送消息,一个终端接收消息。