目录
(4)主机改变所在网络时需要修改IP地址,不可修改MAC地址
(3)把字符串风格的IP地址转为4字节地址 inet_addr ,4字节转字符串 inet_ntoa
(4)本地通信:127.0.0.11——本地环回—代表本主机
④chmod +x udpClient 将程序转为可执行程序
6.windows做客户端,linux做服务器的联网通信 步骤(udp套接字)
一.网络基础
1.认识 "协议"
以寄快递为例:你和卖家沟通好,买一个鼠标,实际上快递员给你的是 一个包裹,里面有鼠标,
实际上多给了我一些东西,多了一张 快递单, 快递单是一块数据=>快递公司和快递点,快递小哥之间的协议。 为了维护协议,一定要在被传输的数据上,新增其他数据(协议数据)
举例:
HTTP协议是超文本传输协议;DNS协议为域名解析协议;FTP协议为文件传输协议;SMTP协议为电子邮件传输协议
2.协议分层
(1)软件分层
软件是可以分层的,为什么要分层?
1.软件在分层的同时,也把问题归类的
2.分层的本质:软件上解耦
3.便于工程师进行软件维护
网络本身的代码,就是层状结构!
(2)协议分层
层状结构下的网络协议,我们认为,同层协议 都可以认为自已在和对方直接通信,忽略底层细节同层之间一定都要有自己的协议。
在下面这个例子中, 我们的协议只有两层(汉语协议和电话机协议); 但是实际的网络通信会更加复杂, 需要分更多的层次。分层最大的好处在于 "封装",面向对象例子。
3.OSI七层模型
4.TCP/IP五层(或四层)模型
5.网络和操作系统之间的关系
(1)体系结构直接决定, 数据包在主机内进行流动的时候,一定是要进行自顶向下(封包)或者自底向上(解包)进行流动的。以前的所有的IO都是这样的。
(2)tcp/ip协议和操作系统之间的关系是:操作系统内部,有一个模块,就叫做tcp/ip协议(传输层和网络层),网络协议栈是隶属于OS的。
(3)同层协议都认为自已在和对方直接通信——所以每一层都要有自己的协议
(4)重谈协议——计算机的视角,如何看待协议:① 体现在代码逻辑上 ② 体现在数据上
以寄快递为例:你和卖家沟通好,买一个鼠标,实际上快递员给你的是一个包裹,里面有鼠标,
实际上多给了我一些东西,多了一张快递单,快递单是一块数据=>快递公司和快递点,快递小哥之间的协议。为了维护协议,一定要在被传输的数据上,新增其他数据(协议数据)
6.数据包的封装(封包)和解包,分用
(1)下图为数据封装,解包的过程
(2)分用
有效载荷的分用过程:数据包添加报头的时候,也要考虑未来解包的时候,将自己的有效载荷交付给上层的哪一个协议!
下图为数据分用的过程:
两个结论:(大部分协议的公共属性)
1.一般而言,任何报头属性里面,一定要存在的一些字段支持,我们进行封装和解包,即:报头中一定要存着用于 区分报头和有效载荷 的数据
2.一般而言,任何报头属性里面,一定要存在的一些字段支持我们进行分用。即:报头中一定要存着用于 得知报文的有效载荷要给上层哪个协议 的数据
(3)示例:
路由器可看做一个主机同时横跨了两个局域网
所有的IP向上的协议,发送和接受主机看到的数据是一模一样的
网络 -> IP网络,IP协议屏蔽了底层网络的差异! ! !
数据“你好”从客户发出,不断封装,到以太网驱动程序完成最后封装,再通过以太网传输给路由器下的以太网驱动程序,路由器下的以太网驱动程序解包数据,传给路由器,路由器发现这个数据是要传给IPB的,再通过路由器下的以太网驱动程序封装,通过令牌环传输给目标主机所在网络,自底向上解包传输
(4)数据包传输通过路由器转发
网络传输数据的本质就是数据不断的封装和解包。路由器可看做一个主机同时横跨了两个局域网
所有的数据,必须在”网线”上跑!
7.局域网(以太网)通信的原理
(1)局城网中两台主机可以互相通信
如果两台主机,处于同一个局城网。这两台主机可以直接通信——以太网,一种局域网的标准(以太——物理学界太空中不存在的物质叫以太,为了致敬命名以太网)以太网:站在系统的角度 就是 两台主机之间的临界资源。
(2)局域网通信原理
1.每一台主机都要有唯一的标识:该主机对应的MAC地址!
2.任何一台主机,在任何时刻,都可以随时发消息——碰撞域——无法准确的听到对应的消息——识别发生了碰撞(碰撞检测)——碰撞避免——等不碰撞了过一会儿再发消息
8.MAC地址和IP地址
(1)生活小例子类比MAC地址和IP地址
1.从哪里来<源IP>,到哪里去<目的IP>——IP地址:源IP,目的IP
2.上一站从哪里来<源mac地址>,下一站要去哪里<目标mac地址>(由“到哪里去<目的IP>”决定)——MAC地址: 源mac地址,目标mac地址
MAC地址:用来在局域网中,标定主机的唯一性。
IP地址:用来在广域网(公网),标定主机的唯一性。
(2)IP地址:
(3)MAC地址:
(4)主机改变所在网络时需要修改IP地址,不可修改MAC地址
(5)IP协议的两个版本, IPv4和IPv6:
二.预备基础知识
1.源IP地址和目的IP地址
目的IP地址:通信主机目的主机
两主机可以在同一个局域网也可以不在。
2.端口号,套接字组成介绍
我们在网络通信的时候,不止是让两台主机通信。实际上,在进行通信的时候,不仅仅要考虑两台主机间互相交互数据。本质上讲,进行数据交互的时候是用户和用户在进行交互。用户的身份,通常是用程序体现的。程序一定是在运行中——进程!
主机间在通信的本质是:在各自的主机上的两个进程在互相交互数据!
IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方
IP :确保主机的唯一性
端口号(port):确保该主机上某一个进程的唯一性(则一个进程只能占用一个端口号)
IP:PORT = 标识互联网中唯一的一个进程!——>这两个合起来叫 socket(套接字)(翻译是插座)
网络通信的本质:就是进程间通信! ! !
3.理解 "端口号" 和 "进程ID"(端口号的意义)
4.源端口号和目的端口号
源IP:源端口, 目的IP:目的端口——两个socket对
5.TCP协议与UDP协议
(1)TCP协议
- 传输层协议
-
有连接(要有建立连接的预备工作)
-
可靠传输(可靠性:丢包重传,数据乱序排序等,但是会做更多工作,比较复杂。使用实例:例如转账不能丢包必须用 TCP协议 )
-
面向字节流
(2)UDP协议
- 传输层协议
-
无连接(不需要建立连接,直接发数据)
-
不可靠传输(虽然无可靠性,但是做的工作很少,是简单协议。使用实例:例如全球直播就用 UDP协议 即可,网好就看,网不好就别看)
-
面向数据报
6.网络字节序
(1)规定:网络字节序默认是大端
(2)网络和主机字节序的转换函数
uint32_t htonl (uint32_ t hostlong); ——htonl(host to net 主机转网络)
三.socket套接字编程接口
socket头文件:
man socket,man htons,man inet_ addr查看所有头文件
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
1.socket 常见API(套接字编程接口)
2.sockaddr结构(套接字的地址结构类型定义)
3.套接字接口
(1)创建一个套接字 socket
man 2 socket
int socket(int domain, int type, int protocol);
domain:socket网络通信的域——网络通信 (AF_INET /PF_INET )(或 本地通信 (AF_UNIX))。现在只用AF_INET 网络通信(有的地方把AF_INET写成PF_INET也是正确的)
type:套接字类型——决定了我们通信的时候对应的报文类型(流式 / 用户数据报式)
流式套接:SOCK_STREAM ——用于TCP协议
用户数据报式套接:SOCK_DGRAM ——用于UDP协议
protocol:协议类型——网络应用中设置为 0。(因为AF_INET+SOCK_STREAM—默认是TCP套接字;AF_INET+SOCK_DGRAM—默认是UDP套接字)
返回值:成功返回文件描述符(套接字描述符),错误返回-1并设置错误码(套接字类型本质就是文件描述符)
(2)绑定网络信息 bind
man 2 bind
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:套接字这个文件描述符。addr:传入我们自己创建的信息 struct sockaddr_in local 的地址,然后把它强转成struct sockaddr类型结构体,内部会自动识别是什么类型的套接字做绑定。addrlen:sockaddr类型结构体 的大小
返回值:成功返回0,失败返回-1
例如:if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
(3)把字符串风格的IP地址转为4字节地址 inet_addr ,4字节转字符串 inet_ntoa
①inet_addr
in_addr_t inet_addr(const char *cp); 把字符串风格的IP地址 cp 转为4字节地址并返回。inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n 主机字节序转网络字节序(使用后就不用再调用htonl了)注意:这类函数在转变IP风格时都会自动进行主机字节序和网络字节序之间的转换。
返回值:成功返回IP对应的网络字节序的数;失败返回INADDR_NONE;
in_addr_t就是4字节类型
②inet_ntoa
char *inet_ntoa(struct in_addr in); 把4字节IP地址转为字符串风格的IP地址并返回。
例子: std::string peerIp = inet_ntoa(peer.sin_addr); //拿到了对方的IP
inet_ntoa不是线程安全的函数
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
(4)网络服务 recvfrom 与 sendto
①udp特有的 recvfrom读取套接字中的信息
man recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
从特定套接字 sockfd中读取数据到缓冲区buf中,buf大小为len,flags设为0——阻塞式读取
src_addr:(输出型参数)当服务器读取客户端发送的消息时——哪个客户端给你发的消息,就把这个客户端套接字信息存入src_addr中。(src_addr的类型是套接字类型指针struct sockaddr*,传入的网络套接字类型struct sockaddr_in*需要强转成此类型指针 struct sockaddr*。)
addrlen:(输入输出型参数)客户端这个缓冲区大小。(socklen_t就是unsigned int)
返回值:返回读到的字节数,错误就返回-1错误码被设置
当客户端使用recvfrom读取服务器返回发送的消息时——src_addr和addrlen没意义,但是还是要定义一个套接字类型结构体添上占位
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;
}
}
}
②sendto 向套接字发送信息
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
通过客户端的指定套接字sockfd,发送buf中的数据,buf的大小是len,flags=0 默认阻塞式发送,
dest_addr:(输入型参数)向哪个主机发消息,套接字类型指针struct sockaddr*,传入的网络套接字类型struct sockaddr*需要强转成此类型指针 struct sockaddr*。
addrlen:(输入型参数)主机这个缓冲区大小。(socklen t就是unsigned int)
返回值:返回读到的字节数,错误就返回-1错误码被设置
(首次调用sendto函数的时候,我们的client会自动bind自己的ip和port)
(5)日志写法(可变参数)
在C/C++中会遇到需要定义使用可变参数的函数,例如printf就是,他的格式就是int printf(const char *format,...),对于这样类型的函数,他的实现实际上就是从format格式的指针指向的空间中读取可变参数的类型,然后根据可变参数的首地址读取相应的可变参数值
va_list ap; va_start 就是char* 指针类型。
void va_start(va_list ap, last); va_start(ap, format);——获取可变参数的首地址并赋值给ap
type va_arg(va_list ap, type); ——提取ap,根据type参数类型获取实参值返回
void va_end(va_list ap); ——将 ap 置空,即将可变参数指针归NULL
void va_copy(va_list dest, va_list src);
int vsnprintf(char *str, size_t size, const char *format, va_list ap); 通过读取format得到可变参数的类型,将用户格式化的可变参数内容写入数组str中
str:把格式化内容写进str这个数组中。size:被写入空间的大小 sizeof(str)-1(不包含'\0')。format:存储 可变参数的类型 的空间。ap:可变部分
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 3
const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};
// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...) level日志等级
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap; // ap -> char*
va_start(ap, format);//用离可变参数format最近的参数初始化ap
vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);
va_end(ap); // ap = NULL
FILE *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;
// }
}
4.部分细节解释+代码(udp套接字)
易错:1. port_ 端口号是一个 2字节16位的整数,主机转网络要用htos,不能用htol(这个错误找了一天呐~)server.sin_port=htons(server_port);
htol 是转换四字节的,如果你传入一个两字节的数据,它就会自动进行补位,补位前面部分都是零,那这时候经过htol置换之后,前16位就变成零了,相当于你的程序跑去绑定零端口去了,就会绑定失败。
(1)INADDR_ANY
#define INADDR_ANY ((in_addr_t) 0x00000000)
local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
①INADDR_ANY(这个宏的值就是0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法(解释:一般服务器只有一个IP,会自动bind这个IP;如果服务器有多个IP,会自动bind这个服务器的所有的IP——因为如果有两个IP:IP1和IP2,只bind一个IP1,那么只有传给IP1的报文会交给程序,IP2就不会提交报文)
云服务器有一些特殊情况:禁止你bind云服务器上的任何确定IP, 所以这里只能使用INADDR_ANY,如果你是虚拟机就可以bind自己虚拟机的IP,用ifconfig查看IP。
注意:这里inet_addr(ip_.c_str()) 当ip_是"0"时 等价于INADDR_ANY,INADDR_ANY 这个宏的值就是0,0是字符串风格还是网络风格无所谓,并且inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序,即 inet_addr(0)=inet_addr(INADDR_ANY)=htonl(INADDR_ANY) 作用是一样的
(2)inet_addr(上面有)
in_addr_t inet_addr(const char *cp); 把字符串风格的IP地址 cp 转为4字节地址并返回。inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用。因为IP地址也是会发给对方的,所以除了做转化,inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序(使用后就不用再调用htonl了)(INADDR_ANY 是0,所以h—>n转不转都行)
(3)bzero
bzero(&local,sizeof(1ocal)); ——bzero函数将从s开始的区域的前n个字节设置为0(字节包含'\0'). 也可以用memset代替
(4)本地通信:127.0.0.11——本地环回—代表本主机
客户端发送消息到本地的网络协议栈,但是不发送到网络,仅通过本地网络协议栈向上交付给另一个进程的缓冲区中。
(5)sock进loop会变成-1的问题
init中创建套接字不能加int,否则sock就是局部变量了
服务器创建dup的流程:
服务器:创建套接字,填充信息,bind绑定,recvfrom等待接收消息,checkOnlineUser 添加在线用户,messageRoute 消息路由
客户端:创建套接字,填充服务器的信息,创建线程去recvfrom等待路由消息,主线程发消息给服务器
Makefile
.PHONY:all
all:udpClient udpServer
udpClient: udpClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpClient udpServer
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);//这个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. 根据命令行,设置要访问的服务器IP
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 2. 创建客户端
// 2.1 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind
// 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
(OS随机申请生成一个进程并让这个进程去绑定运行客户端)
// 如果我非要自己bind呢?可以!严重不推荐!
// 所有的客户端软件 <-> 服务器 通信的时候,必须得有client[ip:port]<->server[ip:port]
// 为什么不需要用户自己bind端口信息呢??client很多,不能给客户端bind指定的port,port
可能被别的client使用了,你的client就无法启动了
// 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!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(sockfd, buffer.c_str(), buffer.size(), 0,
(const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
}
close(sockfd);
return 0;
}
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_in
struct sockaddr_in local; // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中
bzero(&local, sizeof(local)); // 可以用memset代替
// 填充协议家族,域,选择是网络通信还是本地通信
local.sin_family = AF_INET; sin_family就是开头的16位地址类型:AF_ INET
// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
local.sin_port = htons(port_); port_类内成员是本地序列,要用htons转网络序列
// 服务器都必须具有IP地址,42.192.83.143 "xx.yy.zz.aaa" ,字符串风格点分十进制 -> 4字节IP
-> uint32_t ip(每个数字是0~255,8bit)
// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
local.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)
{
//'\0'的值就是0,'0'的值是48,这里是存ASCII为0的'\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); //拿到了对方的IP,因为inet_ntoa这个函数参数类型
就是in_addr而不是in_addr_t,所以参数填peer.sin_addr而不是peer.sin_addr.s_addr
uint32_t peerPort = ntohs(peer.sin_port); // 拿到了对方的port
checkOnlineUser(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_;
// onlineuser
std::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();
5.linux上的联网通信 步骤(udp套接字)
①makefile改成静态编译
②sz udpClient 把客户端发送到桌面
相当于发布软件
③ rz -e 用户下载软件
④chmod +x udpClient 将程序转为可执行程序
⑤在linux上通信可以开始了
打开服务器 ./udpServer,此时服务器阻塞等待有人发消息。各个客户端:./udpClient +服务器的公网IP+8080(端口号),就可以发消息通信了
6.windows做客户端,linux做服务器的联网通信 步骤(udp套接字)
①makefile改成静态编译
windows上的客户端代码框架
#pragma comment(lib, "ws2_32.lib") // 需要包含的链接库
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h> // windows socket 2.2版本
int main()
{
WSADATA wsaData; // 用作初始化套接字
WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化启动信息(初始套接字)
客户端的创建套接字,填充服务器的信息,sendto通信
closesocket(SendingSocket); // 释放套接字
WSACleanup(); // 清空启动信息
system("pause");
return 0;
}
全代码
发送这里的可执行程序就可以通信了