做二维码的网站,湖北聚四方建设有限公司网站,企业注册资金,威海市临港区建设局网站网络编程套接字 预备知识理解源IP地址和目的IP地址认识端口号理解 端口号 和 进程ID理解源端口号和目的端口号认识TCP协议认识UDP协议网络字节序 socket编程接口socket 常见APIsockaddr结构 简单的UDP网络程序UDP通用服务端udp服务端初始化udp服务端启… 网络编程套接字 预备知识理解源IP地址和目的IP地址认识端口号理解 端口号 和 进程ID理解源端口号和目的端口号认识TCP协议认识UDP协议网络字节序 socket编程接口socket 常见APIsockaddr结构 简单的UDP网络程序UDP通用服务端udp服务端初始化udp服务端启动服务端完整代码 UDP通用客户端udp客户端初始化udp客户端启动udp客户端完整代码 地址转换函数 简单的TCP网络程序TCP通用服务端tcp服务端初始化tcp服务端启动tcp服务端完整代码 TCP通用客户端tcp客户端初始化tcp客户端启动tcp客户端完整代码 守护进程 TCP协议通讯流程 预备知识
理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
将数据从主机A发送到主机BIP唯一标识唯一的主机主机A是数据发送端也称源IP地址主机B是数据接受端也称目的IP地址
认识端口号
上述将数据从A主机发送到B主机并不是目的恰恰只是手段真正通信的是机器上的软件进程IP用来标识主机的唯一所以端口就是用来标识主机上进程的唯一性
由此IP地址该主机上的端口号用来标识该服务器上进程的唯一性 所以网络通信的本质就是进程间通信 两主机进行通信的前提是需要看到同一份资源–网络资源通信类似IO将数据发送出去读取收到的数据
端口号(port)是传输层协议的内容
端口号是一个2字节16位的整数端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理IP地址 端口号能够标识网络上的某一台主机的某一个进程一个端口号只能被一个进程占用
理解 “端口号” 和 “进程ID”
进程既然已经有pid为什么还要有port端口号呢
pid是系统层面的port是网络层面的分别设置为了解耦客户需要每次都能够找到进程服务器的唯一性不能进行改变恰好ipport能够满足并不是所有的进程都要提供网络服务但是所有的进程都需要pid
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”
在网络通信中ipport标识唯一性两主机进行通信时发送端不仅要发送数据还要发送一份“多余”的数据也就是自己的ipport这份多余的数据便会以协议的形式进行呈现
认识TCP协议
传输层协议有连接可靠传输面向字节流
认识UDP协议
传输层协议无连接不可靠传输面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
在网络通信中上面这些工作已有操作系统给解决介绍几个接口
#include arpa/inet.h
//主机转网络 long long
uint32_t htonl(uint32_t hostlong);
//主机转网络 short
uint16_t htons(uint16_t hostshort);
//网络转主机 long long
uint32_t ntohl(uint32_t netlong);
//网络转主机 short
uint16_t ntohs(uint16_t netshort);socket编程接口
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);sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议 然而, 各种网络协议的地址格式并不相同
套接字大致分为三种网络套接字原始套接字Unix域间套接字 若实现编程则需要三种不同的接口为了方便使用只设计一套接口通过不同的参数解决所有网络或者其他场景下的通信 结合上面套接字的接口当进行不同的编程时使用不同的结构体只需要进行类型转换通过前两个字节便可判断是类型
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结构体指针做为参数
sockaddr 结构
sockaddr_in 结构
协议家族指明套接字用于通信的类型
端口号
in_port_t sin_port;ip地址需要注意的是这里采取的是4字节来表示ip地址在网络通信中的ip地址形式都是点分十进制例如127.0.0.1所以需要进行类型转换下面会详细介绍
简单的UDP网络程序
UDP通用服务端
udp服务端初始化
创建socketsocket的作用是用来通信的
int socket(int domain, int type, int protocol);AF_INET协议表明此socket是网络通信 SOCK_DGRAM表明通信的数据形式是数据报的类型
需要注意的是创建socket成功返回的是一个文件描述符所以udp通信的本质其实就是进程间通信 _sockfd socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd -1)
{cerr socket error: errno : strerror(errno) endl;exit(SOCKET_ERR);
}
cout socket success: : _sockfd endl;绑定portip未来服务器要明确的port不能随意改变这里就需要上面介绍的sockaddr结构由于是网络通信所以采用的是sockaddr_in结构未来服务端给客户端发消息需要将port和ip要不要发送给对方
struct sockaddr_in local;
bzero(local, sizeof(local));这里的协议与上面不同这里的协议是表明填充sockaddr_in结构用于网络通信
local.sin_family AF_INET;在前面介绍过在网络中数据统一是大端存储这里需要调用API对数据进行转换
local.sin_port htons(_port);网络中的IP地址形式是点分十进制此结构中采用的是4字节来表示所以需要将IP数据类型转换为4字节然后还需要转换为大端同理这里也是采取API
local.sin_addr.s_addr inet_addr(_ip.c_str());一般服务端不会指定特定IP因为一个服务器的一个端口会接受许多不同IP客户端传来的数据如果指定IP会导致其余客户端的数据丢失所以一般使用0.0.0.0用来接受所有数据同一端口
local.sin_addr.s_addr htonl(INADDR_ANY);以上操作只是在用户栈上进行的操作系统并不知晓所以需要进行bind
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);在传入数据时需要进行类型转换以实现统一接口
int n bind(_sockfd, (struct sockaddr*)local, sizeof(local));
if(n -1)
{cerr bind error: errno : strerror(errno) endl;exit(BIND_ERR);
}udp服务端启动
服务器的本质其实就是一个死循环 服务端在未来接受客户端传来的数据时需要知道客户端的端口和IP地址这些数据就是保存在sockaddr_in结构中的接受的过程这些数据是由操作系统自动进行填写通信是双方的既然服务端接受到了数据所以也需要做出回应回应的过程操作类似这里没有进行展示
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);void start()
{// 服务器的本质其实就是一个死循环char buffer[gnum];for (;;){// 读取数据struct sockaddr_in peer;socklen_t len sizeof(peer); ssize_t s recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);// 1. 数据是什么 2. 谁发的if (s 0){buffer[s] 0;string clientip inet_ntoa(peer.sin_addr); // 1. 网络序列 2. int-点分十进制IPuint16_t clientport ntohs(peer.sin_port);string message buffer;cout clientip [ clientport ]# message endl;}}
}服务端完整代码
namespace Server
{using namespace std;static const string defaultIp 0.0.0.0; // TODOstatic const int gnum 1024;enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR};typedef functionvoid(string, uint16_t, string) func_t;class udpServer{public:udpServer(const uint16_t port, const string ip defaultIp): _port(port), _ip(ip), _sockfd(-1){}void initServer(){// 1. 创建socket_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd -1){cerr socket error: errno : strerror(errno) endl;exit(SOCKET_ERR);}cout socket success: : _sockfd endl;// 2. 绑定portip(TODO)// 未来服务器要明确的port不能随意改变struct sockaddr_in local; // 定义了一个变量栈用户bzero(local, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port); // 你如果要给别人发消息你的port和ip要不要发送给对方local.sin_addr.s_addr inet_addr(_ip.c_str()); // 1. string-uint32_t 2. htonl(); - inet_addr// local.sin_addr.s_addr htonl(INADDR_ANY); // 任意地址bind服务器的真实写法int n bind(_sockfd, (struct sockaddr *)local, sizeof(local));if (n -1){cerr bind error: errno : strerror(errno) endl;exit(BIND_ERR);}// UDP Server 的预备工作完成}void start(){// 服务器的本质其实就是一个死循环char buffer[gnum];for (;;){// 读取数据struct sockaddr_in peer;socklen_t len sizeof(peer); // 必填ssize_t s recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);// 1. 数据是什么 2. 谁发的if (s 0){buffer[s] 0;string clientip inet_ntoa(peer.sin_addr); // 1. 网络序列 2. int-点分十进制IPuint16_t clientport ntohs(peer.sin_port);string message buffer;cout clientip [ clientport ]# message endl;}}}~udpServer(){}private:uint16_t _port;string _ip; // 实际上一款网络服务器不建议指明一个IPint _sockfd;};
}UDP通用客户端
客户端时先向服务端发送数据所以需要提前得知IP和端口
udp客户端初始化
与服务端不同的是客户端不需要进行显示bind因为服务端只有一个而客户端却是多个在客户端第一次向服务端发送数据时操作系统会自动对其进行bind填入必要的数据端口和IP
void initClient()
{// 创建socket_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd -1){cerr socket error: errno : strerror(errno) endl;exit(2);}cout socket success: : _sockfd endl;
}udp客户端启动
与服务端不同的是客户端是先向其发送数据然后再接受其反馈反馈过程没有展示
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);void run()
{struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(_serverip.c_str());server.sin_port htons(_serverport);string message;while (!_quit){cout Please Enter# ;cin message;sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)server, sizeof(server));}
}udp客户端完整代码
namespace Client
{using namespace std;class udpClient{public:udpClient(const string serverip, const uint16_t serverport): _serverip(serverip), _serverport(serverport), _sockfd(-1), _quit(false){}void initClient(){// 创建socket_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd -1){cerr socket error: errno : strerror(errno) endl;exit(2);}cout socket success: : _sockfd endl;}void run(){struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(_serverip.c_str());server.sin_port htons(_serverport);string message;while (!_quit){cout Please Enter# ;cin message;sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)server, sizeof(server));}}~udpClient(){}private:int _sockfd;string _serverip;uint16_t _serverport;bool _quit;};
} 展示udp通信过程
地址转换函数
sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址 但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换
字符串转in_addr的函数
in_addr_t inet_addr(const char *cp);in_addr转字符串的函数
char *inet_ntoa(struct in_addr in);简单的TCP网络程序
TCP与UDP的区别是否需要链接通信的数据类型也不同 TCP需要链接通信数据是面向字节流
TCP通用服务端
tcp服务端初始化
与udp服务端不同的是tcp服务需要先进行链接 举个栗子 当你向客服进行询问前提是客服随时都在线即使没有客户进行询问时也必须在线将这种状态称为“监听”tcp服务端也是如此在进行链接之前需要将socket进行监听第二个参数后面会介绍
int listen(int sockfd, int backlog);// 3.设置socket为监听状态
if (listen(_listensock, gbacklog) 0)
{std::cerr listen socket error std::endl;exit(LISTEN_ERR);
}
std::cout listen socket success! std::endl;tcp服务端启动
接下来就是建立链接的过程再举个栗子 在赌场周围会有拉客的人称他们为张三他们的目的就是拉客将你拉入赌场进行消费而当你进入赌场之后真正为你服务的却不是那群拉客的人而是里面的工作人员称作李四这里的张三的作用就只用来拉客李四才是真的服务人员tcp服务端也是如此进行监听的socket在链接成功之后会返回一个新的socket新生成的socket的作用才是用来通信的由于tcp通信是面向字节流所以在链接成功之后接下来的通信本质其实就是文件操作IO操作
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len sizeof(peer);
int sock accept(_listensock, (struct sockaddr *)peer, len);
if (sock 0)
{std::cerr accept error,next errno : strerror(errno) std::endl;continue;
}
std::cout accept success std::endl;tcp服务端完整代码
namespace server
{enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport 8080;static const int gbacklog 5;class TcpServer;class ThreadData{public:ThreadData(TcpServer *self, int sock): _self(self), _sock(sock){}public:TcpServer *_self;int _sock;};class TcpServer{public:TcpServer(const uint16_t port gport): _listensock(-1), _port(port){}~TcpServer(){}void InitServer(){// 1.创建socket文件套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){std::cerr create socket error errno : strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout create socket success! std::endl;// 2.bind绑定自己的网络信息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(_listensock, (struct sockaddr *)local, sizeof(local)) 0){std::cerr bind socket error errno : strerror(errno) std::endl;exit(BIND_ERR);}std::cout bind socket success! std::endl;// 3.设置socket为监听状态if (listen(_listensock, gbacklog) 0){std::cerr listen socket error std::endl;exit(LISTEN_ERR);}std::cout listen socket success! std::endl;}void start(){for (;;){// 4.server获取新链接struct sockaddr_in peer;socklen_t len sizeof(peer);int sock accept(_listensock, (struct sockaddr *)peer, len);if (sock 0){std::cerr accept error,next errno : strerror(errno) std::endl;continue;}std::cout accept success std::endl;// 多线程版pthread_t tid;ThreadData *td new ThreadData(this, sock);pthread_create(tid, nullptr, thread_routine, td);}}static void *thread_routine(void *args){pthread_detach(pthread_self());ThreadData *td static_castThreadData *(args);td-_self-serviceIO(td-_sock);close(td-_sock);delete td;return nullptr;}void serviceIO(int sock){char buffer[1024];while (true){ssize_t n read(sock, buffer, sizeof(buffer) - 1);if (n 0){// 目前为止将读到的数据当成字符串buffer[n] 0;std::cout read message: buffer std::endl;std::string outbuffer buffer;outbuffer server[echo];write(sock, outbuffer.c_str(), outbuffer.size());}else if (n 0){// 数据被读取完毕客户端退出std::cout client quit,me too! std::endl;break;}}close(sock);}private:int _listensock;uint16_t _port;};
}TCP通用客户端
tcp客户端初始化
这个过程与udp一样不加赘述
void Initclient()
{// 1.创建socket_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket create error errno strerror(errno) std::endl;exit(1);}
}tcp客户端启动
既然服务端是监听那么之后客服端发起链接之后二者才能进行链接然后通信
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);if (connect(_sock, (struct sockaddr *)server, sizeof(server)) ! 0)
{std::cerr socket connect error std::endl;
}
else
{std::string msg;while (true){std::cout Enter#;std::getline(std::cin, msg);write(_sock, msg.c_str(), msg.size());char buffer[NUM];int n read(_sock, buffer, sizeof(buffer) - 1);if (n 0){// 目前为止将读取的数据当成字符串buffer[n] 0;std::cout Server回显# buffer std::endl;}else{break;}}
}tcp客户端完整代码
#define NUM 1024
namespace client
{class TcpClient{public:TcpClient(const std::string serverip, const uint16_t serverport): _sock(-1), _serverip(serverip), _serverport(serverport){}void Initclient(){// 1.创建socket_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket create error errno strerror(errno) std::endl;exit(1);}}void start(){struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(_serverport);server.sin_addr.s_addr inet_addr(_serverip.c_str());if (connect(_sock, (struct sockaddr *)server, sizeof(server)) ! 0){std::cerr socket connect error std::endl;}else{std::string msg;while (true){std::cout Enter#;std::getline(std::cin, msg);write(_sock, msg.c_str(), msg.size());char buffer[NUM];int n read(_sock, buffer, sizeof(buffer) - 1);if (n 0){// 目前为止将读取的数据当成字符串buffer[n] 0;std::cout Server回显# buffer std::endl;}else{break;}}}}~TcpClient(){if (_sock 0)close(_sock);}private:int _sock;std::string _serverip;uint16_t _serverport;};
}tcp通信展示
这里还存在一个问题服务器运行之后之前的任何指令都会被当做消息被发送不会再被命令行解释器执行通过键盘可直接将其停止真正的服务器应该是保存在云端一直运行不会受其他因素影响 接下来接受守护进程来解决这个问题
守护进程 xshell链接远端服务器之后服务器立刻形成一个会话有且只有一个前台任务多个后台任务其中bash命令行解释器一般作为前台任务用来执行各种指令 通过命令行解释器形成两个后台任务仔细观察会发现两个任务分别是由两个组长3127131416带领的PGID为组长的代号所有成员的共同领导是前台bash解释器30990
如果将其中一个后台任务转换到前台结果会怎么样呢 由于Bash命令行解释器被切换为了后台所以各种指令任务一都无法执行和上面的情形一致守护进程是自称会话相当于自己既是领导也是组长同时也是员工
模拟实现守护进程 1.使调用进程忽略异常的信号
signal(SIGPIPE, SIG_IGN);如何服务端出现异常客服端向其发送消息时会直接忽略掉异常可以继续发送
2.只有组员才能自成会话setsid 举个栗子在公司里面组长不允许直接离职创业因为他需要管理下面的员工但是员工就可以直接离职创业这里采取的方式是父进程退出子进程自成会话
if (fork() 0)exit(1);// 子进程-》守护进程 本质也是孤儿进程的一种
pid_t id setsid();
assert(id ! -1);3.守护进程是脱离终端的关闭或者重定向之前进程默认打开的文件 进程默认会打开012文件描述符所指向的文件为确保服务器不受器影响需要将其关闭这里采取的是将其重定向到”文件黑洞“可以接受所有指令 int fd open(DEV, O_RDWR);
if (fd 0)
{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);
}
else
{close(0);close(1);close(2);
}完整代码
#define DEV /dev/null
void deamonSelf()
{// 1.使调用进程忽略异常的信号signal(SIGPIPE, SIG_IGN);// 2.只有组员才能自成会话setsidif (fork() 0)exit(1);// 子进程-》守护进程 本质也是孤儿进程的一种pid_t id setsid();assert(id ! -1);// 3.守护进程是脱离终端的关闭或者重定向之前进程默认打开的文件int fd open(DEV, O_RDWR);if (fd 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);}else{close(0);close(1);close(2);}
}服务端运行之后确实立刻自成会话 查看服务端进程被1号进程领养自此网络通信告一段落
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给服务器; (第四次
这个断开连接的过程, 通常称为 四次挥手