目录
1、预备知识
1.1、认识端口号
1.2、端口号 vs 进程pid
1.3、认识TCP协议
1.4、认识UDP协议
1.5、网络字节序
2、socket编程接口
2.1、socket常见API
2.2、sockaddr结构
3、实现一个简易的UDP服务器和客户端通信
log.hpp
UdpServer.hpp
UdpClient.cc
Main.cc
makefile
1、预备知识
1.1、认识端口号
首先我们先来解决一个问题,在进行网络通信的时候,是不是我们的两台机器在进行通信呢?
答案肯定是不是的,在网络协议中的下三层,主要解决的是,数据安全可靠的运送到远端机器,但是使我们的用户使用应用层软件,完成数据发送和接收的,软件对应的自然就是进程,所以,我们日常网络通信的本质:就是进程间通信!
那么问题又来了,我们两台主机在进行通信时,传输层的上层应用层不止有一个应用软件,该如何区分呢?用的就是端口号。
在公网上,IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程,所以:IP:Port = 标识全网唯一的一个进程。
那么我们来总结一下端口号的特性;
· 端口号是一个2字节16位的整数
· 端口号用来标识一个进程,告诉操作系统,当前这个数据要交给哪个进程处理
· IP地址+端口号能够标识网络上的某一台主机的某一个进程
· 一个端口号只能被一个进程占用
1.2、端口号 vs 进程pid
pid已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号?
这个问题也很简单,因为网络是晚于操作系统的,而操作系统的每个进程都需要有pid,但是却不失所有的进程都要网络通信,另外,也是追求软件工程低耦合高内聚思想,创建一个端口号,实现了系统和网络功能的解耦。
再做一点小小的扩充,我们的客户端是如何知道服务器的端口号是多少的?
答:每一个服务的端口号都必须是众所周知,精心设计,被客户端知晓的。
那么这该如何实现呢?
如图所示,其实在传输层,有一张哈希表存储端口号,而进程会绑定哈希表中的端口号,这样就实现了端口号的要求,但是这里还有一些细节:
一个进程可以绑定多个端口号,但是一个端口号不可以被多个进程绑定,如果被多个进程绑定就会让端口号无法决定该将信息发送给哪个进程!
1.3、认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,后面再讨论TCP的细节问题。
· 传输层协议
· 有连接
· 可靠传输
· 面向字节流
1.4、认识UDP协议
此处也是对UDP(User Datagram Protocol 用户数据协议)有一个直观的认识,细节后面讨论。
· 传输层协议
· 无连接
· 不可靠传输
· 面向数据报
1.5、网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
· 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
· 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
· 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
· TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
· 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
· 如果当前发送主机是小端,就需要先将数据转成大端。否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
2、socket编程接口
2.1、socket常见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);
2.2、sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而,各种网络协议的地址格式并不相同。
· IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
· IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
· socket API可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
3、实现一个简易的UDP服务器和客户端通信
log.hpp
#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}// void logmessage(int level, const char *format, ...)// {// time_t t = time(nullptr);// struct tm *ctime = localtime(&t);// char leftbuffer[SIZE];// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),// ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);// // va_list s;// // va_start(s, format);// char rightbuffer[SIZE];// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);// // va_end(s);// // 格式:默认部分+自定义部分// char logtxt[SIZE * 2];// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);// // printf("%s", logtxt); // 暂时打印// printLog(level, logtxt);// }void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暂时打印printLog(level, logtxt);}private:int printMethod;std::string path;
};// int sum(int n, ...)
// {
// va_list s; // char*
// va_start(s, n);// int sum = 0;
// while(n)
// {
// sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
// n--;
// }// va_end(s); //s = NULL
// return sum;
// }
UdpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <string.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"typedef std::function<std::string(const std::string &, const std::string&, uint16_t)> func_t;Log log;enum{SOCKET_ERR=1,BIND_ERR=2
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip), isrunning_(false){}void Init(){// 1. 创建udp socket// Udp的socket是全双工的,允许被同时读写的sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){log(Fatal, "socket creat error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}log(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;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 = htonl(INADDR_ANY);int n = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local));if(n < 0){log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}log(Info, "bind success, errno: %d, err string: %s", 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_.insert({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 = "[";message += clientip;message += ":";message += std::to_string(clientport);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);}}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", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);CheckUser(client, clientip, clientport);std::string info = inbuffer;Broadcast(info, clientip, clientport);}}~UdpServer(){if(sockfd_ > 0) close(sockfd_);}private:int sockfd_; // 网络文件描述符std::string ip_; // 任意地址bind 0uint16_t port_; // 表明服务器进程的端口号bool isrunning_;std::unordered_map<std::string, struct sockaddr_in> online_user_;
};
UdpClient.cc
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"using namespace std;void Usage(std::string proc)
{std::cout << "\n\tUsage:" << proc << "serverip serverport\n" << std::endl;
}struct ThreadData
{struct sockaddr_in server;int sockfd;std::string serverip;
};void *recv_message(void *args)
{//OpenTerminal();ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while(true){memset(buffer, 0, sizeof(nuffer));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;welcome += "coming...";sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);while(true){cout << "Please Enter@ ";getline(cin, message);// 1. 数据 2. 给谁发sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);}
}//多线程
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0){std::cout << "socker error" << endl;return 1;}td.serverip = serverip;// client 要bind吗?要!只不过不需要用户显式的bind!一般由OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候pthread_t recvr, sender;pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(recvr, 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\tUsage:" << proc << "port[1024]\n" << std::endl;
}// ./udpserver port
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;
}
makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc g++ -o $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient
这里实现了一个小型的聊天室
所有人发的聊天消息都会显示在客户端。
每有一位新用户,服务器就会显示添加信息。