目录

1. 网络基础知识

1.1 理解源IP地址和目的IP地址

1.2 认识端口号

1.3 理解 "端口号" 和 "进程ID"

1.3.1 理解源端口号和目的端口号

1.4 认识TCP协议

1.5 认识UDP协议

1.6 网络字节序

2. socket编程接口

2.1 socket 常见API

2.2 sockaddr结构

2.3 sockaddr 结构

2.4 sockaddr_in 结构

2.5 in_addr结构

3. 简单的UDP网络程序

3.1 封装 UdpSocket

3.2 UDP通用服务器

3.3 实现英译汉服务器

3.4 UDP通用客户端

3.5 实现英译汉客户端

3.6 地址转换函数

3.7 关于inet_ntoa

4. udp网络编程

4.1 网络接口函数及测试

4.2 UDP程序完整代码

后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

——By 作者:新晓·故知


本节重点

  • 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
  • 学习socket api的基本用法;
  • 能够实现一个简单的udp客户端/服务器;

1. 网络基础知识

1.1 理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析。

在进行网络通信的时候,不仅需要考虑两台主机间互相交互数据,本质上,进行数据交互是用户与用户在进行交互!而用户的身份通常书程序体现的,程序在运行中就是在进程中,

主机间通信的本质是:在各自的主机上的进程在交互数据!

IP地址可以完成主机与主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方!

1.2 认识端口号

端口号(port)是传输层协议的内容.
  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用.

IP:确保主机的唯一性
port:确保该主机上的进程的唯一性

IP-PORT: 标识互联网中唯一的一个进程       ——> socket  ——>网络通信的本质也是进程间通信!

1.3 理解 "端口号" 和 "进程ID"

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

1.3.1 理解源端口号和目的端口号

送快递例子
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";

1.4 认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

1.5 认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.6 网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
  • 这些函数名很好记,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,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。
  • 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结构体指针做为参数;

2.3 sockaddr 结构

2.4 sockaddr_in 结构

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主
要有三部分信息: 地址类型, 端口号, IP地址.

2.5 in_addr结构

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数

3. 简单的UDP网络程序

实现一个简单的英译汉的功能
备注: 代码中会用到 地址转换函数 . 参考接下来的章节

3.1 封装 UdpSocket

udp_socket.hpp:

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket
{
public:UdpSocket() : fd_(-1){}bool Socket(){fd_ = socket(AF_INET, SOCK_DGRAM, 0);if (fd_ < 0){perror("socket");return false;}return true;}bool Close(){close(fd_);return true;}bool Bind(const std::string &ip, uint16_t port){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = bind(fd_, (sockaddr *)&addr, sizeof(addr));if (ret < 0){perror("bind");return false;}return true;}bool RecvFrom(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL){char tmp[1024 * 10] = {0};sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0, (sockaddr *)&peer, &len);if (read_size < 0){perror("recvfrom");return false;}// 将读到的缓冲区内容放到输出参数中buf->assign(tmp, read_size);if (ip != NULL){*ip = inet_ntoa(peer.sin_addr);}if (port != NULL){*port = ntohs(peer.sin_port);}return true;}bool SendTo(const std::string &buf, const std::string &ip, uint16_t port){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr *)&addr, sizeof(addr));if (write_size < 0){perror("sendto");return false;}return true;}private:int fd_;
};

3.2 UDP通用服务器

udp_server.hpp:
#pragma once
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lambda
#include <functional>
typedef std::function<void(const std::string &, std::string *resp)> Handler;
class UdpServer
{
public:UdpServer(){assert(sock_.Socket());}~UdpServer(){sock_.Close();}bool Start(const std::string &ip, uint16_t port, Handler handler){// 1. 创建 socket// 2. 绑定端口号bool ret = sock_.Bind(ip, port);if (!ret){return false;}// 3. 进入事件循环for (;;){// 4. 尝试读取请求std::string req;std::string remote_ip;uint16_t remote_port = 0;bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);if (!ret){continue;}std::string resp;// 5. 根据请求计算响应handler(req, &resp);// 6. 返回响应给客户端sock_.SendTo(resp, remote_ip, remote_port);printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port, req.c_str(), resp.c_str());}sock_.Close();return true;}private:UdpSocket sock_;
};

3.3 实现英译汉服务器

以上代码是对 udp 服务器进行通用接口的封装. 基于以上封装, 实现一个查字典的服务器就很容易了.
dict_server.cc:
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{auto it = g_dict.find(req);if (it == g_dict.end()){*resp = "未查到!";return;}*resp = it->second;
}
int main(int argc, char *argv[])
{if (argc != 3){printf("Usage ./dict_server [ip] [port]\n");return 1;}// 1. 数据初始化g_dict.insert(std::make_pair("hello", "你好"));g_dict.insert(std::make_pair("world", "世界"));g_dict.insert(std::make_pair("c++", "一种面向对象程序语言"));g_dict.insert(std::make_pair("Linux", "一种操作系统"));// 2. 启动服务器UdpServer server;server.Start(argv[1], atoi(argv[2]), Translate);return 0;
}

3.4 UDP通用客户端

 udp_client.hpp:
#pragma once
#include "udp_socket.hpp"
class UdpClient
{
public:UdpClient(const std::string &ip, uint16_t port) : ip_(ip), port_(port){assert(sock_.Socket());}~UdpClient(){sock_.Close();}bool RecvFrom(std::string *buf){return sock_.RecvFrom(buf);}bool SendTo(const std::string &buf){return sock_.SendTo(buf, ip_, port_);}private:UdpSocket sock_;// 服务器端的 IP 和 端口号std::string ip_;uint16_t port_;
};

3.5 实现英译汉客户端

dict_client.cc:

3.6 地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
 

3.7 关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
多线程调用inet_ntoa代码示例如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;void *Func1(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}return NULL;
}
int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;pthread_create(&tid1, NULL, Func1, &addr1);pthread_t tid2 = 0;pthread_create(&tid2, NULL, Func2, &addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

makefile: 

.PHONY:all
all:dictclient dictserver inetntoadictclient:dict_client.ccg++ -o $@ $^ -std=c++11
dictserver:dict_server.ccg++ -o $@ $^ -std=c++11
inetntoa:inet_ntoa.ccg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -f dictclient dictserver inetntoa

4. udp网络编程

4.1 网络接口函数及测试

测试1:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"class UdpServer
{
public:UdpServer(){}~UdpServer(){}
public:void init(){}void start(){}
private:int sockfd_;
};
int main(int argc, char *argv[])
{// UdpServer svr;// svr.init();// svr.start();int fd=socket(AF_INET,SOCK_DGRAM,0);if(fd < 0){logMessage(FATAL,"%s:%d",strerror(errno),fd);exit(1);}logMessage(DEBUG,"socket create success! fd: %d",fd);return 0;
}

#pragma once#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap; // ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);va_end(ap); // ap = NULLFILE *out = (level == FATAL) ? stderr:stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr),\name == nullptr ? "unknow":name,\logInfo);// char *s = format;// while(s){//     case '%'://         if(*(s+1) == 'd')  int x = va_arg(ap, int);//     break;// }
}

测试2:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1){}~UdpServer(){}public:void init(){// 1. 创建socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件if (sockfd_ < 0){logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);exit(1);}logMessage(DEBUG, "socket create success! sockfd_: %d", sockfd_);// 2. 绑定网络信息,指明ip+port// 2.1 先填充基本信息到 struct sockaddr_instruct sockaddr_in local;     // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中bzero(&local, sizeof(local)); // memset// 填充协议家族,域local.sin_family = AF_INET;// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中local.sin_port = htons(port_);// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>nlocal.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());// 2.2 bind 网络信息if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1){logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);exit(2);}logMessage(DEBUG, "socket bind success! sockfd_: %d", sockfd_);// done}void start(){// 服务器设计的时候,服务器都是死循环// demo1while (true){logMessage(NOTICE, "server 提供 service 中....");sleep(1);}}private:// 服务器必须得有端口号信息uint16_t port_;// 服务器必须得有ip地址std::string ip_;// 服务器的socket fd信息int sockfd_;// onlineuserstd::unordered_map<std::string, struct sockaddr_in> users;
};// struct client{
//     struct sockaddr_in peer;
//     uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }// ./udpServer port [ip]
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3{Usage(argv[0]);exit(3);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}UdpServer svr(port, ip);svr.init();svr.start();return 0;
}

 

4.2 UDP程序完整代码

udpServer.cc:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1){}~UdpServer(){}public:void init(){// 1. 创建socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件if (sockfd_ < 0){logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);exit(1);}logMessage(DEBUG, "socket create success: %d", sockfd_);// 2. 绑定网络信息,指明ip+port// 2.1 先填充基本信息到 struct sockaddr_instruct sockaddr_in local;     // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中bzero(&local, sizeof(local)); // memset// 填充协议家族,域local.sin_family = AF_INET;// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中local.sin_port = htons(port_);// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>nlocal.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());// 2.2 bind 网络信息if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1){logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);exit(2);}logMessage(DEBUG, "socket bind success: %d", sockfd_);// done}void start(){// 服务器设计的时候,服务器都是死循环char inbuffer[1024];  //将来读取到的数据,都放在这里char outbuffer[1024]; //将来发送的数据,都放在这里while (true){struct sockaddr_in peer;      //输出型参数socklen_t len = sizeof(peer); //输入输出型参数// demo2//  UDP无连接的//  对方给你发了消息,你想不想给对方回消息?要的!后面的两个参数是输出型参数ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){inbuffer[s] = 0; //当做字符串}   else if (s == -1){logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);continue;}// 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]std::string peerIp = inet_ntoa(peer.sin_addr);       //拿到了对方的IPuint32_t peerPort = ntohs(peer.sin_port); // 拿到了对方的portcheckOnlineUser(peerIp, peerPort, peer); //如果存在,什么都不做,如果不存在,就添加// 打印出来客户端给服务器发送过来的消息logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);//大小写转化测试// for(int i = 0; i < strlen(inbuffer); i++)// {//     if(isalpha(inbuffer[i]) && islower(inbuffer[i])) outbuffer[i] = toupper(inbuffer[i]);//     else outbuffer[i] = toupper(inbuffer[i]);// }messageRoute(peerIp, peerPort,inbuffer); //消息路由// 线程池!// sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);// demo1// logMessage(NOTICE, "server 提供 service 中....");// sleep(1);}}void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer){std::string key = ip;key += ":";key += std::to_string(port);auto iter = users.find(key);if(iter == users.end()){users.insert({key, peer});}else{// iter->first, iter->second->// do nothing}}void messageRoute(std::string ip, uint32_t port, std::string info){std::string message = "[";message += ip;message += ":";message += std::to_string(port);message += "]# ";message += info;for(auto &user : users){sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));}}private:// 服务器必须得有端口号信息uint16_t port_;// 服务器必须得有ip地址std::string ip_;// 服务器的socket fd信息int sockfd_;// onlineuserstd::unordered_map<std::string, struct sockaddr_in> users;
};// struct client{
//     struct sockaddr_in peer;
//     uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }// ./udpServer port [ip]
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3{Usage(argv[0]);exit(3);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}UdpServer svr(port, ip);svr.init();svr.start();return 0;
}// struct ip
// {
//     uint32_t part1:8;
//     uint32_t part2:8;
//     uint32_t part3:8;
//     uint32_t part4:8;
// }
// struct ip ip_;
// ip_.part1 = s.substr();

udpClient.cc:

#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <unistd.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>struct sockaddr_in server;static void Usage(std::string name)
{std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}void *recverAndPrint(void *args)
{while (true){int sockfd = *(int *)args;char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}
}// ./udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}// 1. 根据命令行,设置要访问的服务器IPstd::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 2. 创建客户端// 2.1 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd > 0);// 2.2 client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind// 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!// 如果我非要自己bind呢?可以!严重不推荐!// 所有的客户端软件 <-> 服务器 通信的时候,必须得有 client[ip:port] <-> server[ip:port]// 为什么呢??client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法启动了// 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!// 2.2 填写服务器对应的信息bzero(&server, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());pthread_t t;pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);// 3. 通讯过程std::string buffer;while (true){std::cerr << "Please Enter# ";std::getline(std::cin, buffer);// 发送消息给server// 首次调用sendto函数的时候,我们的client会自动bind自己的ip和portsendto(sockfd, buffer.c_str(), buffer.size(), 0, (const struct sockaddr *)&server, sizeof(server)); }close(sockfd);return 0;
}

Log.hpp:

#pragma once#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap; // ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);va_end(ap); // ap = NULLFILE *out = (level == FATAL) ? stderr:stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr),\name == nullptr ? "unknow":name,\logInfo);// char *s = format;// while(s){//     case '%'://         if(*(s+1) == 'd')  int x = va_arg(ap, int);//     break;// }
}

makefile:

.PHONY:all
all:udpClient udpServerudpClient:udpClient.ccg++ -o $@ $^ -lpthread -std=c++11
udpServer:udpServer.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f udpClient udpServer

后记:
●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

——By 作者:新晓·故知

<UDP网络编程>——《计算机网络》相关推荐

  1. ComeFuture英伽学院——2020年 全国大学生英语竞赛【C类初赛真题解析】(持续更新)

    视频:ComeFuture英伽学院--2019年 全国大学生英语竞赛[C类初赛真题解析]大小作文--详细解析 课件:[课件]2019年大学生英语竞赛C类初赛.pdf 视频:2020年全国大学生英语竞赛 ...

  2. ComeFuture英伽学院——2019年 全国大学生英语竞赛【C类初赛真题解析】大小作文——详细解析

    视频:ComeFuture英伽学院--2019年 全国大学生英语竞赛[C类初赛真题解析]大小作文--详细解析 课件:[课件]2019年大学生英语竞赛C类初赛.pdf 视频:2020年全国大学生英语竞赛 ...

  3. 信息学奥赛真题解析(玩具谜题)

    玩具谜题(2016年信息学奥赛提高组真题) 题目描述 小南有一套可爱的玩具小人, 它们各有不同的职业.有一天, 这些玩具小人把小南的眼镜藏了起来.小南发现玩具小人们围成了一个圈,它们有的面朝圈内,有的 ...

  4. 信息学奥赛之初赛 第1轮 讲解(01-08课)

    信息学奥赛之初赛讲解 01 计算机概述 系统基本结构 信息学奥赛之初赛讲解 01 计算机概述 系统基本结构_哔哩哔哩_bilibili 信息学奥赛之初赛讲解 02 软件系统 计算机语言 进制转换 信息 ...

  5. 信息学奥赛一本通习题答案(五)

    最近在给小学生做C++的入门培训,用的教程是信息学奥赛一本通,刷题网址 http://ybt.ssoier.cn:8088/index.php 现将部分习题的答案放在博客上,希望能给其他有需要的人带来 ...

  6. 信息学奥赛一本通习题答案(三)

    最近在给小学生做C++的入门培训,用的教程是信息学奥赛一本通,刷题网址 http://ybt.ssoier.cn:8088/index.php 现将部分习题的答案放在博客上,希望能给其他有需要的人带来 ...

  7. 信息学奥赛一本通 提高篇 第六部分 数学基础 相关的真题

    第1章   快速幂 1875:[13NOIP提高组]转圈游戏 信息学奥赛一本通(C++版)在线评测系统 第2 章  素数 第 3 章  约数 第 4 章  同余问题 第 5 章  矩阵乘法 第 6 章 ...

  8. 信息学奥赛一本通题目代码(非题库)

    为了完善自己学c++,很多人都去读相关文献,就比如<信息学奥赛一本通>,可又对题目无从下手,从今天开始,我将把书上的题目一 一的解析下来,可以做参考,如果有错,可以告诉我,将在下次解析里重 ...

  9. 信息学奥赛一本通(C++版) 刷题 记录

    总目录详见:https://blog.csdn.net/mrcrack/article/details/86501716 信息学奥赛一本通(C++版) 刷题 记录 http://ybt.ssoier. ...

  10. 最近公共祖先三种算法详解 + 模板题 建议新手收藏 例题: 信息学奥赛一本通 祖孙询问 距离

    首先什么是最近公共祖先?? 如图:红色节点的祖先为红色的1, 2, 3. 绿色节点的祖先为绿色的1, 2, 3, 4. 他们的最近公共祖先即他们最先相交的地方,如在上图中黄色的点就是他们的最近公共祖先 ...

最新文章

  1. 链接全局变量再说BSS段的清理
  2. 强化学习最新作品:谷歌最新思想、MIT新书推荐、Sutton经典之作!
  3. SQL 简单,复杂查询,基本函数查询
  4. ruby的module与Java的interface以及C++的friend
  5. 【NLP】首个任务型对话系统中生成模块资源库Awesome-TOD-NLG-Survey开源!
  6. VTK:可视化之WindowSize
  7. OpenCV人脸检测并把图片写成avi视频
  8. Git - git tag - 查看当前分支 tag 版本说明
  9. HP-UX 中配置Trusted System
  10. 在拓扑引擎内检测到故障,错误代码255
  11. matlab哈明窗带阻,MATLAB数字滤波器程序 Hamming窗带通滤波器
  12. 对JSP内置对象的部分总结
  13. 记录Hbuilder项目使用xcode离线打包上传苹果商店踩过的坑
  14. 让linux识别html,8 款浏览器对 HTML5 支持评测
  15. Ubuntu下图片转pdf和pdf合并
  16. TMS Sphinx Alexandria Full Source
  17. Cris 的Python笔记(十一):面向对象三大特征之多态
  18. ffmpeg实例,图片转视频,图片放大移动示例解说
  19. 具有大写字母和数字的随机字符串生成
  20. 测评-MAGIX SAMPLITUDE PRO X7数字音频工作站

热门文章

  1. 开源WordPress博客主题二次元风-LoliMeow主题
  2. LVGL笔记10--lv_cont容器
  3. 重绘、重排区别如何避免
  4. arxiv数据_使用neo4j第1部分分析arxiv数据
  5. Java 排序 - 冒泡排序
  6. 小程序流量主怎么赚钱?
  7. vivo冯宇飞:iQOO不请代言人 品牌更亲近互联网用户
  8. c++实现的阻塞队列
  9. 23电工杯数学建模A题
  10. XDOJ 综合题 数字分解排序