目录
一、预备知识
【1.1】理解源IP地址和目的IP地址
【1.2】认识端口号
【1.3】理解 "端口号" 和 "进程ID"
【1.4】理解源端口号和目的端口号
【1.5】认识TCP协议
【1.6】认识UDP协议
二、网络字节序
【2.1】socket编程接口
【2.1.1】socket API
【2.1.2】bind API
【2.1.3】listen API
【2.1.4】accept API
【2.1.5】connect API
【2.1.6】recvfrom API
【2.1.7】sendto API
【2.2】端口转换函数
【2.3】地址转换函数
【2.3】sockaddr结构
【2.3.1】sockaddr 结构
【2.3.2】sockaddr_in 结构
【2.3.3】sockaddr结构专用初始化函数
三、netstat 查看系统网络
【5】UDP实现网络通信
【5.1】Makefile文件代码
【5.2】UdpServer文件代码
【5.2.1】UdpServer.hpp文件代码
【5.2.2】UdpServer.cc文件代码
【5.3】UdpClient文件代码
【5.3.1】UdpClient.hpp文件代码
【5.3.2】UdpClient.cc文件代码
【7】TCP实现网络通信
【7.1】Makefile文件代码
【7.2】Log.hcc文件代码
【7.3】TcpServer文件代码
【7.3.1】TcpServer.hpp文件代码
【7.3.2】TcpServer.cc文件代码
【7.4】TcpClient文件代码
【7.4.1】TcpClient.hpp文件代码
【7.4.2】TcpClient.cc文件代码
【9】TCP协议通讯流程
【10】TCP 和 UDP 对比
Linux网络编程套接字(C++)
一、预备知识
【1.1】理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
【思考】 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析。
为了更好的表示唯一主机服务进程的唯一性,我们采用端口号port,标识服务器进程,客户端进程的唯一性。
【1.2】认识端口号
端口号(port)是传输层协议的内容:
-
端口号是一个2字节16位的整数。
-
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
-
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
-
一个端口号只能被一个进程占用。
ip地址(主机全网唯一性) + 该主机上的端口号,标识服务器上进程的唯一性(ip + PortA ,ip + PortB),网络通信的本质:其实就是进程与进程间的通信,ip保证(全网唯一),port保证(主机唯一)。
【1.3】理解 "端口号" 和 "进程ID"
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
【1.4】理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";
【1.5】认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。
-
传输层协议
-
有连接
-
可靠传输
-
面向字节流
【1.6】认识UDP协议
此处我们也是对UDP(User Datagram Protocol用户数据报协议)有一个直观的认识; 后面再详细讨论。
-
传输层协议
-
无连接
-
不可靠传输
-
面向数据报
二、网络字节序
【2.1】socket编程接口
#include <socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// domain : 域:本地通信、网络通信
// type : 我们socket提供的能力类型
#include <socket.h>
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// socket : 绑定指定的文件描述符
// sockaddr : 参数结构
#include <sys/types.h>
#include <sys/socket.h>
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
#include <sys/types.h>
#include <sys/socket.h>
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
#include <sys/types.h>
#include <sys/socket.h>
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
#include <sys/types.h>
#include <sys/socket.h>
// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
#include <sys/types.h>
#include <sys/socket.h>
// 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
【2.1.1】socket API
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
-
应用程序可以像读写文件一样用read/write在网络上收发数据。
-
如果socket()调用出错则返回-1。 对于IPv4, family参数指定为AF_INET。
-
对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议。
-
protocol参数的介绍从略,指定为0即可。
【2.1.2】bind API
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
-
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号。
-
bind()成功返回0,失败返回-1。
-
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号。
-
前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
【2.1.3】listen API
#include <sys/socket.h>
int listen(int socket, int backlog);
-
listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究。
-
listen()成功返回0,失败返回-1。
【2.1.4】accept API
#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
-
三次握手完成后, 服务器调用accept()接受连接。
-
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
-
addr是一个传出参数,accept()返回时传出客户端的地址和端口号; 如果给addr 参数传NULL,表示不关心客户端的地址。
-
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);。
【2.1.5】connect API
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
-
客户端需要调用connect()连接服务器。
-
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址。
-
connect()成功返回0,出错返回-1。
【2.1.6】recvfrom API
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
-
sockfd:文件描述符(套接字)的网络端口
-
buf:读取到缓冲区
-
len:缓冲区长度
-
flags:读取方式默认为0(阻塞式读取)
-
src_addr:读取到的套接字信息
-
addrlen:套接字长度
【2.1.7】sendto API
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
-
sockfd:文件描述符(套接字)的网络端口
-
buf:发送的缓冲区
-
len:缓冲区长度
-
flags:发送方式默认为0(阻塞式发送)
-
dest_addr:自己的套接字信息发送出去
-
addrlen:套接字长度
【2.2】端口转换函数
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
-
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
-
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
-
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
-
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
-
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
-
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include <arpa/inet.h>
// 函数将无符号整数hostlong从主机字节顺序转换为网络字节顺序.
uint32_t htonl(uint32_t, hostlong);
#include <arpa/inet.h>
// 函数将无符号短整数hostshort从主机字节顺序转换为网络字节顺序.
uint16_t htons(uint16_t, hostshort);
#include <arpa/inet.h>
// 函数将无符号整数netlong从网络字节顺序转换为主机字节顺序.
uint32_t ntohl(uint32_t netlong);
#include <arpa/inet.h>
// 函数将无符号短整数netshort从网络字节顺序转换为主机字节顺序.
uint16_t ntohs(uint16_t netshort);
-
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
-
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
-
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
-
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
【2.3】地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换
inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - Internet地址操作例程
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// inet_addr()函数的作用是:将Internet主机地址cp从IPv4的数字点法转换为网络字节顺序的二进制数据。如果输入无效,INADDR_NONE(通常是-1)返回。使用这个函数是有问题的,因为-1是一个有效的地址(255.255.255.255)。避免使用inet_aton()、inet_pton(3)或getad‐Drinfo(3)提供了一种更清晰的方式来指示错误返回。
in_addr_t inet_addr(const char *cp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_network(const char *cp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// net_ntoa()函数的作用是:将Internet主机地址(以网络字节顺序给出)转换为IPv4点分十进制格式的字符串。字符串以静态方式返回已分配的缓冲区,后续调用将覆盖该缓冲区。
char *inet_ntoa(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
struct in_addr inet_makeaddr(int net, int host);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_lnaof(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_netof(struct in_addr in);
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。
【2.3】sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
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.1】sockaddr 结构
sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:
struct sockaddr {
sa_family_t sin_family; //地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
【2.3.2】sockaddr_in 结构
sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>`中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
struct sockaddr_in {
sa_family_t sin_family; // 地址族(Address Family)
uint16_t sin_port; // 16位TCP\UDP端口号
struct in_addr sin_addr; // 32位ip地址
char sin_zero[8] // 不使用
}
// 该结构体中提到另一个结构体in_addr定义如下:它用来存放32位ip地址
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
}
【2.3.3】sockaddr结构专用初始化函数
#include <strings.h>
// 对struct sockaddr 数据类型做初始化
// bzero()函数将从s开始的区域的前n个字节设置为零(包含'\0'的字节)。
void bzero(void *s, size_t n);
三、netstat 查看系统网络
语法
netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]
功能
Linux netstat 命令用于显示网络状态。
利用 netstat 指令可让你得知整个 Linux 系统的网络情况。
选项
-
-a或--all 显示所有连线中的Socket。
-
-A<网络类型>或--<网络类型> 列出该网络类型连线中的相关地址。
-
-c或--continuous 持续列出网络状态。
-
-C或--cache 显示路由器配置的快取信息。
-
-e或--extend 显示网络其他相关信息。
-
-F或--fib 显示路由缓存。
-
-g或--groups 显示多重广播功能群组组员名单。
-
-h或--help 在线帮助。
-
-i或--interfaces 显示网络界面信息表单。
-
-l或--listening 显示监控中的服务器的Socket。
-
-M或--masquerade 显示伪装的网络连线。
-
-n或--numeric 直接使用IP地址,而不通过域名服务器。
-
-N或--netlink或--symbolic 显示网络硬件外围设备的符号连接名称。
-
-o或--timers 显示计时器。
-
-p或--programs 显示正在使用Socket的程序识别码和程序名称。
-
-r或--route 显示Routing Table。
-
-s或--statistics 显示网络工作信息统计表。
-
-t或--tcp 显示TCP传输协议的连线状况。
-
-u或--udp 显示UDP传输协议的连线状况。
-
-v或--verbose 显示指令执行过程。
-
-V或--version 显示版本信息。
-
-w或--raw 显示RAW传输协议的连线状况。
-
-x或--unix 此参数的效果和指定"-A unix"参数相同。
-
--ip或--inet 此参数的效果和指定"-A inet"参数相同。
【备注】如果想要看到更详细的添加sudo
[shaxiang@VM-8-14-centos 99_Lesson_20230707_CodeExecute_UdpNetWork_Linux]$ netstat -nuap // 监控端口号和IP
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 0.0.0.0:68 0.0.0.0:* -
udp 0 0 10.0.8.14:123 0.0.0.0:* -
udp 0 0 127.0.0.1:123 0.0.0.0:* -
udp6 0 0 fe80::5054:ff:fec6::123 :::* -
udp6 0 0 ::1:123 :::* -
[shaxiang@VM-8-14-centos 20230811_TcpNewWork]$ netstat -nltp // 监控TCP服务器
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:45638 0.0.0.0:* LISTEN 20145/node
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 4410/./TcpServer
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp6 0 0 :::3306 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
【5】UDP实现网络通信
【5.1】Makefile文件代码
# 定义变量并且赋值相应的字符串信息
cc := g++
standard := -std=c++11
compile: udpServer udpClient
udpServer: UdpServer.cc
$(cc) -o $@ $^ $(standard)
udpClient: UdpClient.cc
$(cc) -o $@ $^ $(standard)
clean:
rm -rf udpServer udpClient
# .PHONY: 可以避免与系统的命令冲突
.PHONY: compile clean
【5.2】UdpServer文件代码
【5.2.1】UdpServer.hpp文件代码
#pragma once
/* C头文件包含 */
#include <cstdlib>
#include <cstring>
#include <cerrno>
/* C++头文件包含 */
#include <iostream>
#include <functional>
#include <string>
/* 系统头文件包含 */
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
/* UDP服务器Demo封装命名空间 */
namespace Server
{
enum { USAGE_ERR = 1, SOCKET_ERR = 2, BIND_ERR = 3 };
const int g_num = 1024;
/* UDP服务器命名空间 */
class UdpServer
{
private:
using func_t = std::function<void(const int&, const std::string&, const uint16_t&, const std::string&)>;
public:
/* 构造函数 */
UdpServer(const func_t& func, const uint16_t& port)
: _selfSockFd(-1)
, _selfSockProt(port)
, _callBack(func)
{}
/* 析构函数 */
~UdpServer()
{}
public:
/* 初始化 */
void Init()
{
// 创建socket(打开网卡驱动文件)
_selfSockFd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
if(_selfSockFd < 0)
{
std::cerr << "socket error " << errno << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "socket success..." << std::endl;
// 绑定自己的Ip和端口号
struct sockaddr_in localAddr; // 套接字对象
bzero(&localAddr, sizeof(localAddr)); // 初始化
localAddr.sin_family = AF_INET; // 地址家族
localAddr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
localAddr.sin_port = htons(_selfSockProt); // 端口号
int bindState = bind(_selfSockFd, (struct sockaddr*)& localAddr, sizeof(localAddr)); // 绑定套接字
if(bindState < 0)
{
std::cerr << "bind error " << errno << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind success..." << std::endl;
}
/* 启动服务 */
void Start()
{
// 启动
char reBuffer[g_num] = "\0";
while(true)
{
struct sockaddr_in clientAddr; // 套接字对象
bzero(&clientAddr, sizeof(clientAddr)); // 初始化
socklen_t addrLength = sizeof(clientAddr);
// 读取
int reCnt = recvfrom(_selfSockFd, reBuffer, sizeof(reBuffer) - 1, 0, (struct sockaddr*)& clientAddr, &addrLength);
if(reCnt > 0)
{
reBuffer[reCnt] = '\0'; // 处理字符串
std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 获取客户端ip
uint16_t clientPort = ntohs(clientAddr.sin_port); // 获取客户端port
// 调用回调函数
_callBack(_selfSockFd, clientIp, clientPort, reBuffer);
}
}
}
private:
uint16_t _selfSockProt; // 服务器(自己)UDP通讯端口号
int _selfSockFd; // 服务器(自己)UDP通讯文件描述符
func_t _callBack; // 服务器(自己)UDP回调函数
};
};
【5.2.2】UdpServer.cc文件代码
#include <memory>
#include "UdpServer.hpp"
using namespace Server;
/* 函数接口:用户启动提示 */
void Usage(char* argv)
{
std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}
/* 函数回调:通讯功能 */
void CallbackFunction(const int& selfSockFd, const std::string& clientIp, const uint16_t& clientPort, const std::string& message)
{
// 打印接收信息
std::cout << "client ip[" << clientIp << "] " << "port[" << clientPort << "]: " << message << std::endl;
}
/* 函数接口:程序入口 */
int main(int argc, char** argv)
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 服务器对象交管给智能指针
uint16_t userPort = atoi(argv[1]);
std::unique_ptr<UdpServer> udpSevr(new UdpServer(CallbackFunction, userPort));
udpSevr->Init();
udpSevr->Start();
return 0;
}
【5.3】UdpClient文件代码
【5.3.1】UdpClient.hpp文件代码
#pragma once
/* C头文件包含 */
#include <string>
#include <cstring>
/* C++头文件包含 */
#include <iostream>
/* 系统头文件包含 */
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
/* UDP客户端Demo封装命名空间 */
namespace Client
{
enum { USAGE_ERR = 1, SOCKET_ERR = 2, BIND_ERR = 3 };
const int g_num = 1024;
/* UDP客户端命名空间 */
class UdpClient
{
public:
/* 构造函数 */
UdpClient(const uint16_t& port, const std::string& ip)
: _selfSockFd(-1)
, _sevrSockIp(ip)
, _sevrSockProt(port)
{}
/* 析构函数 */
~UdpClient()
{}
public:
/* 初始化 */
void Init()
{
// 创建socket(打开网卡驱动文件)
_selfSockFd = socket(AF_INET, SOCK_DGRAM, 0);
if(_selfSockFd < 0)
{
std::cerr << "socket error " << errno << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "socket success..." << std::endl;
}
/* 启动服务 */
void Start()
{
struct sockaddr_in serverAddr; // 套接字对象
bzero(&serverAddr, sizeof(serverAddr)); // 初始化
serverAddr.sin_family = AF_INET; // 地址家族
serverAddr.sin_addr.s_addr = inet_addr(_sevrSockIp.c_str()); // 服务器ip
serverAddr.sin_port = htons(_sevrSockProt); // 服务器port
// 启动
char stoBuffer[g_num];
while(true)
{
std::cout << "Please Say: ";
std::cin.getline(stoBuffer, g_num);
// 发送信息
sendto(_selfSockFd, stoBuffer, sizeof(stoBuffer), 0, (struct sockaddr*)& serverAddr, sizeof(serverAddr));
}
}
private:
std::string _sevrSockIp; // 服务器(对方)UDP通讯Ip地址
uint16_t _sevrSockProt; // 服务器(对方)UDP通讯端口号
int _selfSockFd; // 客户端(自己)UDP通讯文件描述符
};
};
【5.3.2】UdpClient.cc文件代码
#include <memory>
#include "UdpClient.hpp"
using namespace Client;
/* 函数接口:用户启动提示 */
void Usage(char* argv)
{
std::cout << "Usage:\n\t" << argv << " ServerIp" << " ServerPort\n\n" << std::endl;
}
/* 函数接口:程序入口 */
int main(int argc, char** argv)
{
if(argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 客户端对象交管给智能指针
uint16_t userPort = atoi(argv[2]);
std::string userIp = argv[1];
std::unique_ptr<UdpClient> udpClit(new UdpClient(userPort, userIp));
udpClit->Init();
udpClit->Start();
return 0;
}
【7】TCP实现网络通信
【7.1】Makefile文件代码
# 定义变量并且赋值相应的字符串信息
cc := g++
standard := -std=c++11
compile:tcpServer tcpClient
tcpServer:TcpServer.cc
$(cc) -o $@ $^ $(standard)
tcpClient:TcpClient.cc
$(cc) -o $@ $^ $(standard)
clean:
rm -rf tcpServer tcpClient
# .PHONY: 可以避免与系统的命令冲突
.PHONY:clean compile
【7.2】Log.hcc文件代码
#pragma once
#include <iostream>
#define DEBUG 0 // 调试等级
#define NORMAL 1 // 正常等级
#define WARNING 2 // 警告等级
#define ERROR 3 // 错误等级
#define FATAL 4 // 致命等级
/* 函数:日志等级转为字符串 */
void LevelToString(const int& level)
{
}
/* 函数:日志打印 */
void LogMessage(const int& level, const std::string& message)
{
// 格式:[日志等级] [时间戳/时间] [pid] [信息]
// 比如:[WARNING] [2023-05-11 18:09:23] [12345] [创建socket文件描述符失败!]
std::cout << message << std::endl;
}
【7.3】TcpServer文件代码
【7.3.1】TcpServer.hpp文件代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include "Log.hpp"
namespace Server
{
/* 枚举常量 */
enum{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR
};
const int g_backLog = 10;
const int g_num = 1024;
class TcpServer
{
public:
/* 构造函数 */
TcpServer(const uint16_t& port)
: _selfListenFd(-1)
, _selfPort(port)
{}
/* 析构函数 */
~TcpServer() {}
public:
/* 初始化 */
void Init()
{
// No.1 创建套socket文件套接字对象
_selfListenFd = socket(AF_INET, SOCK_STREAM, 0);
if(_selfListenFd < 0)
{
LogMessage(FATAL, "create socket fail!");
exit(SOCKET_ERR);
}
LogMessage(NORMAL, "create socket success...");
// No.2 绑定自己的网络信息
struct sockaddr_in localAddr; // 创建sockAddr对象
bzero(&localAddr, sizeof(localAddr)); // 初始化对象
localAddr.sin_family = AF_INET; // 绑定协议家族
localAddr.sin_addr.s_addr = INADDR_ANY; // 绑定回环地址[0.0.0.0]
localAddr.sin_port = htons(_selfPort); // 绑定端口号
int bindState = bind(_selfListenFd, (struct sockaddr*)&localAddr, sizeof(localAddr));
if(bindState < 0)
{
LogMessage(FATAL, "bind socket fail!");
exit(BIND_ERR);
}
LogMessage(NORMAL, "bind socket success...");
// No.3 开始监听网络
int listenState = listen(_selfListenFd, g_backLog);
if(listenState < 0)
{
LogMessage(FATAL, "listen socket fail!");
exit(LISTEN_ERR);
}
LogMessage(NORMAL, "listen socket success...");
}
/* 启动 */
void Start()
{
// 运行服务器
while(true)
{
// No.4 获取新连接
struct sockaddr_in clientAddr; // sockAddr对象
socklen_t addrLen = sizeof(clientAddr); // 求长度
bzero(&clientAddr, sizeof(clientAddr)); // 初始化
// 接收链接
_selfSocketFd = accept(_selfListenFd, (struct sockaddr*)&clientAddr, &addrLen);
if(_selfListenFd < 0)
{
LogMessage(ERROR, "accept socket fail!");
exit(ACCEPT_ERR);
}
LogMessage(NORMAL, "accept socket success...");
std::cout << "listenFd: " << _selfListenFd << " " << "sockFd: " << _selfSocketFd << std::endl;
// 面向字节流的读取(对文件进行读取)
ServiceIO();
close(_selfSocketFd); // 必须关闭,防止文件描述符泄露!
break;
}
}
/* 面向字节流读取消息 */
void ServiceIO()
{
while(true)
{
// 接收
char inBuffer[g_num] = { '\0' };
ssize_t n = read(_selfSocketFd, inBuffer, sizeof(inBuffer) - 1);
if(n > 0)
{
// 处理读取到的内容
inBuffer[n] = '\n';
std::cout << "recv buffer: " << inBuffer << std::endl;
// 响应
std::string outBuffer;
outBuffer = "Server echo# ";
outBuffer += inBuffer;
write(_selfSocketFd, outBuffer.c_str(), outBuffer.size());
}
else if(n == 0) // 在读取的时候,如果读取到了0,说明客户端已经退出了,这时候服务器也可以退出了!
{
LogMessage(NORMAL, "client quit me too...");
break;
}
}
}
private:
int _selfListenFd; // TCP通讯(自己)网络监听文件描述符
int _selfSocketFd; // TCP通讯(自己)网络服务文件描述符
uint16_t _selfPort; // TCP通讯(自己)网络端口
};
};
【7.3.2】TcpServer.cc文件代码
#include <memory>
#include "TcpServer.hpp"
using namespace Server;
/* 函数:消息提示 */
void Usage(char* argv)
{
std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}
/* 函数:程序入口函数 */
int main(int argc, char** argv)
{
// 检查用户启动命令
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 将服务器对象转交给智能指针进行管理
// 获取用户指定的端口号
uint16_t userPort = atoi(argv[1]);
std::unique_ptr<TcpServer> pTcpSevr(new TcpServer(userPort));
pTcpSevr->Init();
pTcpSevr->Start();
return 0;
}
【7.4】TcpClient文件代码
【7.4.1】TcpClient.hpp文件代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include "Log.hpp"
namespace Client
{
/* 枚举常量 */
enum{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR,
CONNECT_ERR
};
const int g_num = 1024;
class TcpClient
{
public:
/* 构造函数 */
TcpClient(const std::string& sevrIp, const uint16_t& sevrPort)
: _selfSocketFd(-1)
, _sevrSocketIp(sevrIp)
, _sevrSocketPort(sevrPort)
{}
/* 析构函数 */
~TcpClient() {}
public:
/* 初始化 */
void Init()
{
// No.1 创建套socket文件套接字对象
_selfSocketFd = socket(AF_INET, SOCK_STREAM, 0);
if(_selfSocketFd < 0)
{
LogMessage(FATAL, "create socket fail!");
exit(SOCKET_ERR);
}
LogMessage(NORMAL, "create socket success...");
// No.2 创建链接
struct sockaddr_in local; // 创建sockAddr对象
bzero(&local, sizeof(local)); // 初始化对象
local.sin_family = AF_INET; // 绑定协议家族
local.sin_addr.s_addr = inet_addr(_sevrSocketIp.c_str()); // 绑定服务器Ip
local.sin_port = htons(_sevrSocketPort); // 绑定服务器端口号
int connectState = connect(_selfSocketFd, (struct sockaddr*)&local, sizeof(local));
if(connectState < 0)
{
LogMessage(FATAL, "connect socket fail!");
exit(CONNECT_ERR);
}
LogMessage(NORMAL, "connect socket success...");
}
/* 启动 */
void Start()
{
// 运行
while(true)
{
// 响应
std::cout << "Plase Say# ";
std::string outBuffer;
std::getline(std::cin, outBuffer);
write(_selfSocketFd, outBuffer.c_str(), outBuffer.size());
// 等待回复
char inBuffer[g_num] = { '\0' };
int n = read(_selfSocketFd, inBuffer, sizeof(inBuffer) - 1);
if(n > 0)
{
// 处理读取到的内容
inBuffer[n] = '\0';
std::cout << inBuffer << std::endl;
}
else if(n == 0) // 在读取的时候,如果读取到了0,说明服务器已经退出了,这时候客户端也可以退出了!
{
LogMessage(NORMAL, "server quit me too...");
break;
}
}
close(_selfSocketFd);
}
private:
int _selfSocketFd;
std::string _sevrSocketIp;
uint16_t _sevrSocketPort;
};
};
【7.4.2】TcpClient.cc文件代码
#include <memory>
#include "TcpClient.hpp"
using namespace Client;
/* 函数:消息提示 */
void Usage(char* argv)
{
std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}
/* 函数:程序入口函数 */
int main(int argc, char** argv)
{
// 检查用户启动命令
if(argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 将客户端对象转交给智能指针进行管理
// 获取用户指定的IP地址和端口号
std::string userIp = argv[1];
uint16_t userPort = atoi(argv[2]);
std::unique_ptr<TcpClient> pTcpClit(new TcpClient(userIp, userPort));
pTcpClit->Init();
pTcpClit->Start();
return 0;
}
【9】TCP协议通讯流程
【服务器初始化】
-
调用socket, 创建文件描述符。
-
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失 败。
-
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备。
-
调用accecpt, 并阻塞, 等待客户端连接过来。
【建立连接的过程 】
-
调用socket, 创建文件描述符。
-
调用connect, 向服务器发起连接请求。
-
connect会发出SYN段并阻塞等待服务器应答; (第一次)。
-
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)。
-
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)。
这个建立连接的过程, 通常称为 三次握手。
【数据传输的过程 】
-
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据。
-
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待。
-
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答。
-
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求。
-
客户端收到后从read()返回, 发送下一条请求,如此循环下去。
【断开连接的过程】
-
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)。
-
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)。
-
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)。
-
客户端收到FIN, 再返回一个ACK给服务器; (第四次) 。
这个断开连接的过程, 通常称为四次挥手 。
在学习socket API时要注意应用程序和TCP协议层是如何交互的。
-
应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段。
-
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些 段,再比如read()返回0就表明收到了FIN段 。
【10】TCP 和 UDP 对比
可靠传输 | 不可靠传输 |
---|---|
TCP通讯 | UDP通讯 |
有链接 | 无连接 |
字节流 | 数据包 |