目录
????前言
这篇文章给大家带来套接字的学习!!!
????1、背景知识
????1.1、理解源IP地址和目的IP地址
源IP地址和目的IP地址
-
在IP数据包头部中,有两个IP地址,分别叫做:源IP地址和目的IP地址
-
源IP地址就是标定广域网(公网)中主机唯一性的地址(发送数据方的IP地址)
-
目的IP地址就是“接收数据方”的IP地址(同样在广域网中具有唯一性)
我们只要有IP地址就能完成通信了吗?
-
不是的,想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上
-
但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析
-
IP地址只能帮我们找到对方主机,我们还要考虑与对方相互交互数据
-
比如:西游记中唐太宗要唐僧去西天拜见如来佛祖取得真经,唐僧不是去到西天就行了,他还要拜见如来佛祖取得真经,并且交付给唐太宗,光去到西天是没有用的!!!
????1.2、端口号
端口号(port)是传输层协议的内容
-
端口号是一个2个字节16位的整数
-
端口号用来标识一个主机上某个进程的唯一性
-
IP地址 + 端口号能够标识网络上的某一台主机的某一个唯一的进程(互联网中唯一的进程)
-
一个端口号只能被一个进程占用(唯一性,一般指服务器),但是多个进程可以服务相同的端口号
-
网络通信的本质其实也是进程间通信,都是二个进程在交互数据。本质上数据交互是用户与用户进行交互,用户的身份,通常是由程序(进程)体现的!
端口号和进程PID的关系
-
端口号是标定网络上唯一的进程,进程PID是标定主机上唯一的进程
-
实现时为了不将这二个概念强关联(强耦合),所以单独维护了一个端口号标定网络上的唯一进程,因为不是每个进程都是要进网络的!
源端口号和目的端口号
-
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号
-
源端口描述的是:数据是谁发的,目的端口描述的是:要将数据发给谁!
????1.3、认识UDP协议
UDP(User Datagram Protocol 用户数据报协议)
特点:
-
传输层协议
-
无连接
-
不可靠传输
-
面向数据报
????1.4、认识TCP协议
TCP(Transmission Control Protocol 传输控制协议)
特点:
-
传输层协议
-
有连接
-
可靠传输
-
面向字节流
????1.5、网络字节序
前言
- 大端:数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
- 小端:数据的低位保存在内存的低地址中,而数据的高位, 保存在内存的高地址中
-
内存中的多字节数据相对于内存地址有大端和小端之分
-
磁盘文件中的多字节数据相对于文件中的地址也有大端小端之分
-
网络数据流同样有大端小端之分
如何定义网络数据流的地址呢?
-
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
-
接收主机把从网络上接收的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
-
网络数据流的地址是这样规定的:先发出的数据是低地址,后发出的数据是高地址
-
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节
-
不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
-
总之,如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可,发送到网络后,数据传输到对方时,还要将网络字节序转换成对方主机的存储模式
网络字节序和主机字节序的转换
#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是主机转网络字节序,参数为长整数,htons参数则是一个短整型
-
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
-
ntohl是网络转主机字节序,参数为长整型,ntohs参数是一个短整型
-
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
-
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
????2、套接字相关API
????2.1、socket常见API
下面是套接字的常见API,后面会详细解析
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 创建 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、struct sockaddr 结构体
上面几个API中都出现了struct sockaddr结构体指针,它是什么呢?有什么用?
-
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,比如:IPv4、IPv6,以及UNIX Domain Socket(本地进程间通信)
-
不同的网络协议,它们之间的地址类型是不同的,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址
-
IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6
-
我们只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容,最后确定是什么网络协议
-
总之,sockaddr就是一个通用的类型,其他不同的类型的可以强转成它,然后通过前十六位地址来判断是什么网络通信协议,最后再强转回对应的结构体
-
比如:我们要用IPv4进行通信,定义一个sockaddr_in,填充里面的信息,传参时,只要强转会sockaddr结构体就行了,底层会判断它是什么协议,然后重新强转回去
sockaddr 结构
sockaddr_in 结构体
-
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型, 端口号, IP地址
-
sa_family:它就是我们要填充的地址类型(协议家族)
-
sin_port:它就是要我们填充的端口号
-
sin_addr:它就是需要我们填充的IP地址
in_addr 结构体
- in_addr:用来表示一个IPv4的IP地址。其实就是一个32位的整数,直接赋值即可
????3、UDP网络编程
????3.1、API
使用UDP网络通信所用到的接口比TCP少,因为它是不用进行连接的
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 创建 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);
// 读取网络数据到指定的缓冲区当中 -- 并且将对方的struct sockaddr信息填充到src_addr当中
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// 将缓冲区的内容通过网络发送到对方主机(根据端口号和IP地址发送)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 指定填充确定IP地址, 转化字符串风格IP("xx.zz.yy.sss"),并且自动进行主机字节序转换网络字节序
in_addr_t inet_addr(const char *cp));
int socket(int domain, int type, int protocol);
-
作用:它是用来创建套接字文件的,创建成功返回一个文件描述符,失败返回-1,并且设置errno
-
domain:它需要我们填充协议家族(地址类型)来指定网络协议
-
type:它需要我们填充通信类型,UDP协议是面向数据报的,我们填充SOCK_DGRAM即可,如果是TCP协议,那么填充SOCK_STREAM(其他详细查看man手册)
-
protocol:套接口所用的协议,一般为0,不指定
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
-
作用:绑定网络信息(地址类型、端口号和IP地址)到内核中,成功返回0,错误返回-1,并且设置errno
-
address:sockaddr_in或sockaddr_un的地址,并且需要强转为sockaddr*
-
len:sockaddr_in或sockaddr_un结构体所占内存空间的大小
????3.2、简单的UDP网络程序
实现小写英文转大写服务器 – 使用UDP协议
#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class UdpServer
{
public:
UdpServer(uint16_t port, string ip = "")
: port_(port), ip_(ip), sockfd_(-1)
{
assert(port > 1024);
}
~UdpServer()
{
if (sockfd_ > 2)
{
close(sockfd_);
}
}
public:
void Init()
{
// 1、创建socket套接字 -- 创建通信端点并返回文件描述符fd
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
cout << "socket error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
// 填充网络基本信息到struct sockaddr_in结构体中 -- 地址类型、port、ip address
struct sockaddr_in Server;
socklen_t len = sizeof(Server);
memset(&Server, 0, len);
// 协议家族 -> 填套接字的通信域 -> AF_INET
Server.sin_family = AF_INET;
// 端口号 -> 要发给对方(服务器) -> 一定会到网络中 -> 主机大小端序列转成网络序列(htons转换)
Server.sin_port = htons(port_);
// 服务器IP地址,格式: "xx.yy.zz.vvv"(字符串风格"点分十进制") -> 去掉点就是4字节IP -> uint32_t IP
// inet_addr:指定填充确定IP地址, 转化字符串风格IP,并且自动进行主机序列转网络序列
// 不关心绑定到哪一个IP地址,表示不确定地址,或“所有地址”、“任意地址” -- 即:OS可以随意分配本机ip地址(本机可能存在多个网卡)
Server.sin_addr.s_addr = (ip_.empty() ? htonl(INADDR_ANY) : (inet_addr(ip_.c_str())));
// 2、绑定网络信息到内核中
if (bind(sockfd_, ((const struct sockaddr *)&Server), len) < 0)
{
cout << "bind error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
}
void Start()
{
#define BUFFER 1024
char buffer[BUFFER];
char outbuffer[BUFFER];
struct sockaddr Client;
socklen_t len = sizeof(Client);
while (true)
{
memset(buffer, 0, BUFFER);
// 接收客户端发送的消息
ssize_t index = recvfrom(sockfd_, (void *)buffer, BUFFER - 1, 0, (struct sockaddr *)&Client, &len);
if (index < 0)
{
cout << "recvfrom error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
else
{
buffer[index] = '\0';
cout << "Client echo# " << buffer << endl;
}
// 小写英文转大写 -- 服务
memset(outbuffer, 0, BUFFER);
for (int i = 0; i < strlen(buffer); ++i)
{
if (buffer[i] >= 'a' && buffer[i] <= 'z')
{
outbuffer[i] = buffer[i] - 32;
}
else
{
outbuffer[i] = buffer[i];
}
}
outbuffer[strlen(buffer)] = '\0';
// 答复Client -- 发送消息给客户端
ssize_t st = sendto(sockfd_, outbuffer, BUFFER - 1, 0, (const struct sockaddr *)&Client, len);
if (st < 0)
{
cout << "sendto error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
}
}
private:
uint16_t port_;
string ip_;
int sockfd_;
};
int main(int argc, char *argv[])
{
if (!(argc >= 2 && argc <= 3))
{
cout << "Usage: ./udpserver port [ip]" << endl;
exit(3);
}
uint16_t port = atoi(argv[1]);
string ip = "";
if (argc == 3)
{
ip = argv[2];
}
UdpServer us(port, ip);
us.Init();
us.Start();
return 0;
}
该程序在填充网络信息中的IP地址时用了INADDR_ANY,是为什么呢?
-
INADDR_ANY就是指定地址为0.0.0.0的IP地址,这个地址事实上表示不确定地址,或所有地址、任意地址
-
多网卡的状况下,这个就表示全部网卡ip地址的意思
-
如果使用云服务器进行测试,必须使用它,因为云服务器不是给用户真正的提供公网IP,而是内网IP
-
服务器在绑定ip地址时,如果为INADDR_ANY,那么服务器端的IP地址能够随意配置,这样使得该服务器端程序能够运行在任意计算机上,可以使任意计算机做为服务器,便于程序移植
简单客户端代码
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cerrno>
#include <string>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class UdpClient
{
public:
UdpClient(uint16_t port, string ip = "")
: port_(port), ip_(ip), sockfd_(-1)
{
}
~UdpClient()
{
if (sockfd_ > 2)
{
close(sockfd_);
}
}
public:
void Init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
cout << "socket error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
// 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不能随便改变!
}
void Start()
{
#define BUFFER 1024
string outbuffer;
char inbuffer[BUFFER];
while (true)
{
memset(inbuffer, 0, BUFFER);
// 发送请求给服务器 -- 需要对方网络套接字信息
struct sockaddr_in si;
socklen_t len = sizeof(si);
si.sin_family = AF_INET;
si.sin_port = htons(port_);
si.sin_addr.s_addr = inet_addr(ip_.c_str());
cout << "Client: Please enter the sending information# ";
fflush(stdout);
getline(cin, outbuffer);
ssize_t st = sendto(sockfd_, outbuffer.c_str(), BUFFER - 1, 0, (const struct sockaddr *)&si, len);
if (st < 0)
{
cout << "sendto error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
//---------------------------------------------------------------------------------------------
// 获取Server回应消息
int rf = recvfrom(sockfd_, inbuffer, BUFFER - 1, 0, nullptr, 0);
if (rf >= 0)
{
inbuffer[rf] = '\0';
cout << "Server echo# " << inbuffer << endl;
}
else
{
cout << "recvfrom error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
}
}
private:
uint16_t port_;
string ip_;
int sockfd_;
};
// ./ 端口号 ip地址
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage: ./udpserver port [ip]" << endl;
exit(3);
}
// 获取ip地址和端口号
uint16_t server_port = atoi(argv[1]);
std::string server_ip = (argv[2]);
UdpClient uc(server_port, server_ip);
uc.Init();
uc.Start();
return 0;
}
运行结果
- 这里用的是虚拟机测试的,客户端ip地址使用127.0.0.1 – 本地环回地址 – 用于同一台主机通信测试
????3.3、简单UDP多人聊天室服务器
简单思路,具体看代码实现
-
思路:新增一个哈希表,key值为客户端的端口号和IP地址,value为struct sockaddr_in(后续需要通过它来进行信息路由,sendto需要sockaddr_in的地址)
-
服务器正常接收信息,如果有用户发送信息了,那么添加一个函数判断这个用户是否存在于哈希表中(根据key值判断),不存在则inset,存在跳过
-
添加到哈希表后,最后一步就是消息路由到全部用户客户端上了,这里就是遍历哈希表,使用sendto进行消息路由到全部用户中即可
#include <iostream>
#include <unordered_map>
#include <cerrno>
#include <cassert>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class UdpServer
{
public:
UdpServer(uint16_t port, string ip = "")
: port_(port), ip_(ip), sockfd_(-1)
{
assert(port > 1024);
}
~UdpServer()
{
if (sockfd_ > 2)
{
close(sockfd_);
}
}
public:
void Init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
cout << "socket error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
struct sockaddr_in Server;
socklen_t len = sizeof(Server);
memset(&Server, 0, len);
Server.sin_family = AF_INET;
Server.sin_port = htons(port_);
Server.sin_addr.s_addr = (ip_.empty() ? htonl(INADDR_ANY) : (inet_addr(ip_.c_str())));
if (bind(sockfd_, ((const struct sockaddr *)&Server), len) < 0)
{
cout << "bind error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
}
void Start()
{
#define BUFFER 1024
char buffer[BUFFER];
struct sockaddr_in Client;
socklen_t len = sizeof(Client);
while (true)
{
memset(buffer, 0, BUFFER);
ssize_t index = recvfrom(sockfd_, (void *)buffer, BUFFER - 1, 0, (struct sockaddr *)&Client, &len);
if (index < 0)
{
cout << "recvfrom error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
else
{
buffer[index] = '\0';
cout << "Client echo# " << buffer << endl;
}
string ClientPort = std::to_string(Client.sin_port);
string ClientIp = inet_ntoa(Client.sin_addr);
checkOnlineUser(Client, ClientPort, ClientIp); // 添加用户到哈希表, 存在跳过
messageRoute(buffer, ClientPort, ClientIp); // 消息路由
}
}
public:
void checkOnlineUser(const struct sockaddr_in &si, const string &port, const string &ip)
{
string key = port;
key += ':';
key += ip;
auto It = UserIn.find(key);
if (It == UserIn.end())
{
UserIn.insert({key, si});
}
}
void messageRoute(const char *Clientmessage, const string &port, const string &ip)
{
// 路由信息 -- 拿到对应ip、端口号和发送的信息
string message = "[";
message += port;
message += ":";
message += ip;
message += "]# ";
message += Clientmessage;
for (auto kv : UserIn)
{
// 回复Client -- 消息路由到在聊天室的每个用户
ssize_t st = sendto(sockfd_, message.c_str(), BUFFER - 1, 0,
(const struct sockaddr *)&(kv.second), sizeof(kv.second));
if (st < 0)
{
cout << "sendto error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
}
}
private:
uint16_t port_;
string ip_;
int sockfd_;
// key: port:ip | value: sockaddr_in
unordered_map<string, struct sockaddr_in> UserIn;
};
// ./ port ip
int main(int argc, char *argv[])
{
if (!(argc >= 2 && argc <= 3))
{
cout << "Usage: ./udpserver port [ip]" << endl;
exit(3);
}
uint16_t port = atoi(argv[1]);
string ip = "";
if (argc == 3)
{
ip = argv[2];
}
UdpServer us(port, ip);
us.Init();
us.Start();
return 0;
}
客户端
-
新增一个线程,用于获取服务器反馈的消息,如果不并发的去获取,在下一层发送消息前,会被一直阻塞住,这样是获取不到其他用户发送的信息的!!!
-
在类中添加线程执行函数需要加static修饰,因为线程执行函数不能有隐藏this指针,也可以在类外定义,创建线程需要将类的this指针传参给线程执行函数中的参数,否则访问不了类成员
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cerrno>
#include <string>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
#define BUFFER 1024
class UdpClient
{
public:
UdpClient(uint16_t port, string ip = "")
: port_(port), ip_(ip), sockfd_(-1)
{
}
~UdpClient()
{
if (sockfd_ > 2)
{
close(sockfd_);
}
}
public:
void Init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
cout << "socket error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
// 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不能随便改变!
}
void Start()
{
string outbuffer;
while (true)
{
// 发送请求给服务器 -- 需要对方网络套接字信息
struct sockaddr_in si;
socklen_t len = sizeof(si);
si.sin_family = AF_INET;
si.sin_port = htons(port_);
si.sin_addr.s_addr = inet_addr(ip_.c_str());
usleep(10000);
// cout << "Client echo# ";
// fflush(stdout);
getline(cin, outbuffer);
ssize_t st = sendto(sockfd_, outbuffer.c_str(), BUFFER - 1, 0, (const struct sockaddr *)&si, len);
if (st < 0)
{
cout << "sendto error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
//---------------------------------------------------------------------------------------------
StartThread(); // 启动线程服务
}
}
public:
void StartThread()
{
IsStartThread = false; // 防止重复申请线程
if (IsStartThread == false)
{
IsStartThread = true;
tid = pthread_create(&tid, nullptr, ThreadHandler, this); // 并发接收信息,防止看不到其他用户的消息
if (tid < 0)
{
cout << "pthread_create error: " << strerror(errno) << endl;
exit(5);
}
}
}
static void *ThreadHandler(void *arg)
{
UdpClient *const This = static_cast<UdpClient *const>(arg);
char inbuffer[BUFFER];
while (true)
{
memset(inbuffer, 0, BUFFER);
// 获取Server回应消息
int rf = recvfrom(This->sockfd_, inbuffer, BUFFER - 1, 0, nullptr, 0);
if (rf >= 0)
{
inbuffer[rf] = '\0';
cout << "Server echo# " << inbuffer << endl;
}
else
{
cout << "recvfrom error: " << strerror(errno) << endl;
exit(EXIT_FAILURE);
}
}
}
private:
uint16_t port_;
string ip_;
int sockfd_;
pthread_t tid;
bool IsStartThread;
};
// ./ 端口号 ip地址
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage: ./udpserver port [ip]" << endl;
exit(3);
}
// 获取ip地址和端口号
uint16_t server_port = atoi(argv[1]);
std::string server_ip = (argv[2]);
UdpClient uc(server_port, server_ip);
uc.Init();
uc.Start();
return 0;
}
运行结果
-
最左边的是服务器程序,右边二个是客户端程序
-
注意:因为用户要发送消息才会添加到哈希表中,所以第一次发送消息是不会路由到从来没有说话的用户中的!!!