网络(一)
目录
1. 网络基础(一);
2. 网络套接字;
3. TCP实现;
1. 网络基础(一)
1.1 网络发展:
从一个个计算器都是独立的, 到计算机连接起来进行数据共享, 后期计算机数量很多通过交换器和路由器进行传输(局域网). 广域网就是世界各个计算器进行数据共享, 也是由一个个局域网组成.
1.2 初始协议
就是一种约定, 网络之间数据流通就是依据一套协议的.
1.3 协议分层:
每一层进行数据的处理, 分层处理比较好处理数据, 以及维护为一层即可, 代码健壮性好, 鲁棒性好.
OSI七层协议: 应用层, 表示层, 会话层, 传输层, 网络层, 数据链路层, 物理层.
TCP/IP协议: 应用层, 传输层, 网络层, 数据链路层, 物理层.
1.4 网络传输基本流程:
传输通过协议的每一层进行传递. 应用层->传输层->网络层->数据链路层->物理层. 到达对端的物理层向上交付.
2. 网络套接字:
2.1 源ip地址和目的ip地址:
由于通信时候一台主机需要传递信息给对端主机, 就需要找到对端主机的ip地址, 而且本主机找到对端主机是否收到, 传递回来的信息也需要源ip地址来响应回来信息. 始终不会改变的.
2.2 源mac地址和目的mac地址:
数据链路层中的报头中, mac地址只在局域网中有效, 如果出局域网(路由器进行转接)那么就会改变源mac地址和目的mac地址.路由器会将源mac地址报头换成新的报头. 会根据局域网变化改变.
2.3 端口号:
找到对端主机之后, 不仅是信息的交流还有完成某种服务, 那么这样的服务就是一个进程, 标志进程在网络中使用的就是端口号. 本质就是向进程发起请求. 用来标识一台主机的进程. 是属于传输层的内容. 包含2字节16位的整数, 一个端口号只能标识一个进程, 但是一个进程可以被多个端口号标识.
PID也可以标识进程,为啥不用这个呢?
这个相当于PID是用来操作系统里面标识进程的唯一性, 而PORT是在网络中标识进程的唯一性, 操作系统中有一部分是不需要进行网络通信的, 这样场景就不太合适. 在不同场景中PID和PORT是具有自己的合适场景.
2.4 网络字节序:
网络中也存在大小端的问题, 会导致数据读取时候不正确, 导致数据解析时候错误. 这时候大小端很重要辣.
大端: 数据的高字节位保存在内存的低地址位置, 数据的低字节位保存在内存的高地址位置.
小端: 数据的低字节位保存在内存的低地址位置, 数据的高字节位保存在内存的高地址位置.
举例: 0x12345:
所以在网络中规定数据流采用大端字节序. 如果发送端是小端要变大端再发送. 接收端如果是小端就要将在网络中大端的变成小段给接收端读取.
介绍几个网络字节序和主机字节序转换的函数:
h代表主机端字节序, n代表网络端字节序, l是32位4字节的长度, s是16位2字节的长度.
2.5 socket编程接口:
(1) 创建套接字:
domain是协议家族, 本地通信填AF_UNIX, IPV4填AF_INET; IPV6填AF_INET6;
type是服务类型, tcp是使用SOCK_STREAM或者udp是使用SOCK_DGRAM.
protocol协议类别: 一般填0就是默认.
创建成功就是返回正数, 失败返回-1.
(2) 绑定端口号:
sockfd是套接字的标识, addr是网络相关的属性信息, addrlen是addr结构体的长度.
成功就是正数, 失败为-1, 错误码被标志.
(3) 监听套接字:
(4) 接收套接字:
(5) 建立连接:
2.6 sockaddr结构:
套接字不仅可以本地通信还可以网络通信, sockaddr_in由于网络通信, sockaddr_un用于本地通信. sockaddr可以用于本地和网络通信;
sockaddr属于什么类型接口?
我们是在应用层进行程序编写, 然后就是调用的接口就是底层操作系统的系统接口.
sockaddr底层做什么的?
sockaddr是被进程调用的, 然后进程会维护一个进程地址空间PCB, 以及文件描述符.
在pcb中的文件队列中进行打开具体网络文件. 每个文件又有文件缓冲区, 磁盘中读取文件, 操作系统会定期从文件缓冲区中读取数据到网卡中.
2.7 简单UDP实现:
(1) 初始化和析构:
构造函数创建套接字, 以及使用析构函数关闭套接字.
class UdpServer
{
public:
bool InitServer()
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0){ //创建套接字失败
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
return true;
}
~UdpServer()
{
if (_sockfd >= 0){
close(_sockfd);
}
};
private:
int _sockfd; //文件描述符
};
(2) 服务器绑定:
因为套接字创建好无法找到数据是写入磁盘还是网卡中, 进行绑定.
绑定的时候需要将ip地址和网络文件告诉网络文件, 改变文件对应指向网卡, 然后就是将文件和网卡进行绑定起来.
字符串ip转整数ip:
整数ip转字符串ip:
class UdpServer
{
public:
UdpServer(std::string ip, int port)
:_sockfd(-1)
,_port(port)
,_ip(ip)
{};
bool InitServer()
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0){ //创建套接字失败
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
struct sockaddr_in local;
memset(&local, '\0', sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
if(bind(_sockfd, (struct sockaddr*)&local, sizeof(sockaddr)) < 0)
{
cout << "bind error!" << endl;
return false;
}
cout << "bind success" << endl;
return true;
}
~UdpServer()
{
if (_sockfd >= 0){
close(_sockfd);
}
};
private:
int _sockfd; //文件描述符
int _port;
std::string _ip;
};
(3) 运行服务器:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd: 套接字, buf是读取数据存放的位置, len是读取的长度, flag是读取的方式, 0是阻塞读取, src_addr是网络相关的属性信息, addrlen是读取src_addr的长度.可以获取对端主机的IP以及端口号.
void Start()
{
#define SIZE 128
char buffer[SIZE];
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(_sockfd, &buffer, sizeof(buffer)-1, 0, (sockaddr*)&peer, &len);
if(size > 0)
{
buffer[size] = '\0';
int port = ntohs(peer.sin_port);
string ip = inet_ntoa(peer.sin_addr);
cout << ip << ":" << port << "#" << buffer << endl;
}
else
{
cerr << "recvfrom fail" << endl;
}
}
}
netstat指令: 可以查看网络状况:
-n: 直接使用ip地址.
-u: 监控udp;
-t: 监控tcp;
-l:显示监控中的服务器的Socket
-p: 显示正在使用Socket的程序识别码和程序名称
(4) 客户端初始化:
class UdpClient
{
public:
UdpClient(std::string server_ip, int server_port)
:_sockfd(-1)
,_server_port(server_port)
,_server_ip(server_ip)
{}
~UdpClient()
{
if (_sockfd >= 0){
close(_sockfd);
}
}
private:
int _sockfd; //文件描述符
int _server_port; //服务端端口号
std::string _server_ip; //服务端IP地址
};
(5) 客户端绑定问题:
服务器需要绑定端口号, ip地址, 因为服务器需要让别人找到它的ip和端口号去访问服务, 客户端却不需要, 客户端在访问服务器时候只要唯一即可, 不需要绑定. 如果客户端绑定端口号会导致端口号只能给这个客户端其他的无法使用, 那别的客户端如何使用只能等这个客户端使用. 所以非常不好, 操作系统会给客户端分配端口号来标识唯一性.
(6) 客户端发送数据:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:套接字; buf: 待写入数据的存放位置, len: 写入数据的大小, flag写入方式,dest_addr:对端网络相关的属性信息; addrlen: 对端属性大小.
class UdpClient
{
public:
void Start()
{
std::string msg;
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
for (;;){
std::cout << "Please Enter# ";
getline(std::cin, msg);
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
}
}
private:
int _sockfd; //文件描述符
int _server_port; //服务端端口号
std::string _server_ip; //服务端IP地址
};
(7) INADDR_ANY:
如果让服务器被外网访问, 就需要使用INADDR_ANY, 因为我们的ip不是真正的公网ip, 如果使用INADDR_ANY就会如下图给服务器.
class UdpServer
{
public:
bool InitServer()
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0){ //创建套接字失败
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
//填充网络通信相关信息
struct sockaddr_in local;
memset(&local, '\0', sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; //绑定INADDR_ANY
//绑定
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(sockaddr)) < 0){ //绑定失败
std::cerr << "bind error" << std::endl;
return false;
}
std::cout << "bind success" << std::endl;
return true;
}
private:
int _sockfd; //文件描述符
int _port; //端口号
std::string _ip; //IP地址
};
(8) 回声服务器:
就是服务器多一个返回响应的消息, sendto. 客户端接收这个响应. recvfrom.
//服务器:
void Start()
{
#define SIZE 128
char buffer[SIZE];
for (;;){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if (size > 0){
buffer[size] = '\0';
int port = ntohs(peer.sin_port);
std::string ip = inet_ntoa(peer.sin_addr);
std::cout << ip << ":" << port << "# " << buffer << std::endl;
}
else{
std::cerr << "recvfrom error" << std::endl;
}
std::string echo_msg = "server get!->";
echo_msg += buffer;
sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, len);
}
}
//客户端:
void Start()
{
std::string msg;
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
for (;;){
std::cout << "Please Enter# ";
getline(std::cin, msg);
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
#define SIZE 128
char buffer[SIZE];
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len);
if (size > 0){
buffer[size] = '\0';
std::cout << buffer << std::endl;
}
}
}
3. TCP
3.1 实现:
(1) 服务器创建套接字:
在sock创建时候, type是SOCK_STREAM不是SOCK_DGRAM.
class TcpServer
{
public:
void InitServer()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
~TcpServer()
{
if (_sock >= 0){
close(_sock);
}
}
private:
int _sock; //套接字
};
(2) 服务器绑定:
和udp差不多. 绑定有时候失败就是由于资源还没有释放干净或者端口号被其他进程绑定了.还有就是1024一下的端口都被绑定, 尽量绑定8080以上的端口号这些. 还有就是云服务器没有开发端口.
class TcpServer
{
public:
TcpServer(int port)
: _sock(-1)
, _port(port)
{}
void InitServer()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
//绑定
struct sockaddr_in local;
memset(&local, '\0', sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
exit(3);
}
}
~TcpServer()
{
if (_sock >= 0){
close(_sock);
}
}
private:
int _sock; //监听套接字
int _port; //端口号
};
(3) 服务器监听:
TCP进行绑定之后就是进行连接, 连接成功才可以通信, 就是要进行监听连接状况.
sockfd: 监听套接字, backlog就是全连接队列, 多个客户端同时进行连接请求, 就会被放到全连接队列中, 参数代表最大连接个数. 成功就是返回0, 失败返回-1.
#define BACKLOG 5
class TcpServer
{
public:
void InitServer()
{
//创建套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
//绑定
struct sockaddr_in local;
memset(&local, '\0', sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
exit(3);
}
//监听
if (listen(_listen_sock, BACKLOG) < 0){
std::cerr << "listen error" << std::endl;
exit(4);
}
}
private:
int _listen_sock; //监听套接字
int _port; //端口号
};
(4) 服务器获取连接:
sockfd:监听套接字, addr: 对端网络相关的属性信息, addrlen: 网络属性大小. 成功返回对端套接字fd, 失败为-1.
返回的套接字和监听套接字有啥不同?
监听套接字获取客户端的连接, 连接套接字是真正提供服务的套接字后期使用的也是这个.
class TcpServer
{
public:
void Start()
{
for (;;){
//获取连接
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
std::cerr << "accept error, continue next" << std::endl;
continue;
}
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
std::cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<std::endl;
}
}
private:
int _listen_sock; //监听套接字
int _port; //端口号
};
telnet命令: 连接一个服务器;
(5) 服务器处理请求:
进行读取数据, fd:文件描述符, accept返回值就是这个, buf是读取数据放在的地点, count是数据个数. 返回值> 0代表读取到数据, 返回值 == 0代表对端关闭, 返回值<0代表读取失败.
这里读完需要关闭套接字, 防止内存泄漏.
将读取到的数据进行写入数据,buf是写入的数据, count写入的大小.
(6) 客户端创建套接字:
class TcpClient
{
public:
TcpClient(std::string server_ip, int server_port)
: _sock(-1)
, _server_ip(server_ip)
, _server_port(server_port)
{}
void InitClient()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
~TcpClient()
{
if (_sock >= 0){
close(_sock);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
(6) 客户端连接服务器:
sockfd:是套接字, addr: 对端网络相关的属性信息, addrlen: 是网络属性大小. 服务器发起监听和获取连接, 就是捕捉客户端连接, 以及建立连接. 成功返回0, 失败返回-1.
class TcpClient
{
public:
void Start()
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
std::cout << "connect success..." << std::endl;
Request(); //发起请求
}
else{ //connect error
std::cerr << "connect failed..." << std::endl;
exit(3);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
(7) 客户端发起请求:
先进行写入数据给服务器, 然后在进行将数据客户端读取一次.
class TcpClient
{
public:
void Request()
{
std::string msg;
char buffer[1024];
while (true){
std::cout << "Please Enter# ";
getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
if (size > 0){
buffer[size] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
else if (size == 0){
std::cout << "server close!" << std::endl;
break;
}
else{
std::cerr << "read error!" << std::endl;
break;
}
}
}
void Start()
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
std::cout << "connect success..." << std::endl;
Request(); //发起请求
}
else{ //connect error
std::cerr << "connect failed..." << std::endl;
exit(3);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
上述tcp的弊端:
只允许一个客户端进行消息交流, 另外一个客户端只能等上个客户端退出才能进行数据交流.
客户端为什么会显示连接成功?
因为在进行accept的时候连接放在全连接队列中, 但是还没有被使用.
(8) 多进程版TCP:
父进程创建后, 子进程继承父进程的连接服务. 父进程就可以继续进行监听连接, 子进程进行连接服务即可. 由于父进程需要等待子进程退出不然就僵尸进程., 内存泄漏. wait进行迭代子进程. 其实也可以不需要等待子进程, 子进程创建孙子进程, 孙子进程来提供网络服务.
爷爷进程: 获取客户端连接请求;
爸爸进程: 爷爷调用fork创建的进程;
孙子进程: 爸爸进程fork出来的进程, 来实现网络服务的.
爸爸进程创建孙子进程就退出, 那么爷爷进程就等待成功, 爷爷进程可以接着进程accept.
不用等待孙子进程退出, 操作系统可以系统回收孤儿进程.
父子进程之间的文件描述符是互相不干扰的, 爷爷进程创建爸爸进程之后就可以关闭文件描述符了, 爸爸进程创建孙子进程之后也要关闭. 关闭才不会导致资源泄漏.
void Start()
{
for(;;)
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0)
{
cerr << "accept fail!" << endl;
continue;
}
string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
cout << "get a new link->" << sock << "[" << client_ip << "]:" << client_port << endl;
pid_t id = fork();//创建爸爸进程;
if(id == 0)
{
close(_listen_sock);//关闭爷爷文件描述符;
if(fork() > 0)
{
exit(0);//爸爸进程退出;
}
Service(sock, client_ip, client_port);//孙子进程进行网络服务;
exit(0);//孙子进程提供完服务退出.
}
close(sock); //关闭爸爸文件描述符;
waitpid(id, nullptr, 0);//等待爸爸进程退出.
}
}
(9) 多线程版TCP:
创建和维护进程的成本比较高, 线程比较少些, 因为线程本质在进程地址空间运行, 共享大部分的资源, 可以使用线程来提供网络服务. 可以不用等待线程结束, 使用线程分离, 然后accept其他客户端. 共享同一张文件描述符. 但是新线程不知道客户端对应哪个文件描述符, 所以主线程需要告诉它.
由于是共用一张文件描述符表, 主线程无法关闭文件描述符, 只有新线程使用完才可以.
static void* HandlerRequest(void* arg)
{
pthread_detach(pthread_self());
Param* p = (Param*)arg;
Service(p->_sock, p->_ip, p->_port);
delete p;//析构参数申请的堆空间.
return nullptr;
}
void Start()
{
for(;;)
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0)
{
cerr << "accept fail!" << endl;
continue;
}
string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
cout << "get a new link->" << sock << "[" << client_ip << "]:" << client_port << endl;
Param* p = new Param(sock, client_ip, client_port);
pthread_t tid;
pthread_create(&tid, nullptr, HandlerRequest, p);
}
}
3.2 TCP英翻汉服务器:
前面有多线程版本的TCP, 改变一下Headler即可.就可以是一个TCP英翻汉服务器.
class Handler
{
public:
Handler()
{}
~Handler()
{}
void operator()(int sock, std::string client_ip, int client_port)
{
unordered_map<string, string> dict;
dict.insert(make_pair("hello", "你好"));
dict.insert(make_pair("world", "世界"));
dict.insert(make_pair("love", "爱"));
char buffer[1024];
string value;
while(true)
{
size_t size = read(sock, buffer, sizeof(buffer)-1);
if(size > 0)
{
buffer[size] = '\0';
cout << client_ip << ":" << client_port << "#" << buffer << endl;
string key = key;
auto it = dict.find(key);
if(it != dict.end())
{
value = it->second;
}
else
{
value = key;
}
write(sock, value.c_str(), value.size());
}
else if(size == 0)
{
cout << client_ip << ":" << client_port << "close!" << endl;
break;
}
else
{
cerr << sock << "read fail!" << endl;
break;
}
}
close(sock);
cout << client_ip << ":" << client_port << "Service done!" << endl;
}
};
3.3 TCP通信流程:
TCP客户端和服务器端的通信图:
(1) 三次握手:
服务器进程创建套接字, 绑定, 监听套接字的初始化, 就可以开始accpet阻塞获取客户端连接, 客户端进行创建套接字, 然后进行connection, 向服务器发起第一次连接请求, 发起SYN阻塞等待服务器应答, 服务器收到客户端的SYN就会响应SYN+ACK(接收到请求以及同意连接)为第二次握手, 客户端收到服务器的响应给服务器一个应答(ACK)为第三次握手.
(2) 数据传输:
客户端发起数据请求, 服务器接收到返回响应然后数据可以一同发送过去, 或者应答发出后再发服务器数据, 其次客户端给出服务器的应答.
服务器调用accpet之后进行read读取数据, 客户端进行写入数据, 服务器收到后进行响应, 处理网络服务后再write返回数据客户端, 客户端再进行read读取服务器的响应.
(3) 四次挥手:
客户端没有更多的请求了就会发送服务器FIN断开连接请求, 调用close进行关闭为第一次挥手, 其次就是服务器接收到客户端FIN请求进行响应ACK同意断开, 并且将read置0标识对端关闭为第二次挥手, read返回后客户端也知道自己要关闭连接, 给客户端发送FIN断开请求为第三次挥手, 客户端接收之后就返回给服务器ACK为第四次挥手.
断开连接, 目的就是释放资源, 因为维护连接也需要系统资源, 连接多了不断开释放就会内存泄漏. 资源浪费.
3.4 TCP对比UDP:
TCP: 可靠,面向连接和字节流的.
UDP: 不可靠, 面向无连接和数据报.