目录
1.预备知识
1.1理解源IP地址和目的IP地址
IP地址(公网IP)唯一的标识互联网中的一台主机
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
源ip地址:指的就是发送数据包的那个电脑的ip地址
目的ip地址:就是你想要发送到的那个电脑的ip地址
源IP,目的IP:对一个报文来讲,从哪来,到哪里去
最大的意义:指导一个报文该如何进行路径选择
到哪里去:本质就是让我们根据目标,进行路径选择的依据!
目的地址:下一跳(mac地址的变化)
1.2认识端口号
数据从A主机到达B主机,不是目的!
数据到目标主机B的一个进程,提供数据处理的服务
数据刚开始的时候,从哪里来?是在计算机上凭空产生的吗?
计算机本身不产生数据,产生数据的是人!人是通过特定的客户端,产生数据!
本质上,所有的网络通信,站在普通人的角度,都是人和人之间通信
技术人员的视角,我们学到的网络通信,本质上:是进程间通信!
例如:app客户端《—》服务器(也是一个进程)
IP仅仅是解决了两台物理机器之间互相通信
但是我们还要考虑如何保证双方用户之间能看到发送的和接受的数据IP地址(公网IP)唯一的标识互联网中的一台主机呢?
端口号:唯一的标识一台机器上的唯一的一个进程!
IP地址(公网IP)唯一的标识互联网中的一台主机
—》
IP+PORT = 能够标识互联网中的唯一的一个进程!
整个网络看作是一个大的OS,所有的网络上网行为,基本都是在这一个大的OS内,进行进程间通信!
IP+PORT端口号 = 套接字
1.3理解 “端口号” 和 “进程ID”
进程具有独立性,进程间通信的前提工作:先让不同的进程,看到同一份资源!(网络)
问题:一个进程可以关联多个端口号吗?可以
一个端口号可以关联多个进程吗?不可以
pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程
1.4认识TCP协议
TCP(Transmission Control Protocol 传输控制协议)
传输层协议
有连接
可靠传输
面向字节流
发送数据时,要进行深入沟通,报文,如果丢失该如何?该找谁?找到了之后应该如何?
各种事情都要仔细问清楚,然后再将数据转发出去!如果数据丢失了,就会回来重新获得一份数据,然后再发送。
1.5认识UDP协议
UDP(User Datagram Protocol 用户数据报协议)
传输层协议
无连接
不可靠传输
面向数据报
发送数据时,只会送,中间出现一系列问题,都不会解决!
1.6网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
1.发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
2.接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
3.因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
4.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
5.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
6.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
2.socket编程接口
2.1socket常见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);
网络通信的标准方式有很多种,基于ip的网络通信(AF_INET),原始套接字,域间套接字(AF_UNIX),,,
为了使系统结构统一化–》sockaddr
2.2udp服务器的简单应用场景
2.2.1sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
2.2.2编写server
1.创建套接字
#include<iostream>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
int main()
{
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
std::cerr<<"socket create errno"<<errno<<std::endl;
return 1;
}
std::cout<<"sock:"<<sock<<std::endl;
return 0;
}
作为一个服务器,要不要让客户知道,对应的服务器的地址(ip+port)?要
服务器socket信息(ip+port),必须要让客户知道!
一般服务器的port,必须是众所周知(不仅仅是人,也可以被各种软件,app,浏览器等)的,而且轻易不能被改编!
2.给该服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);//此处的端口号,是我们计算机上的变量,是主机序列
//a.需要将人识别的点分十进制,字符串风格ip地址,转换成四字节整数IP
//b.也要考虑大小端
// in_addr_t inet_addr(const char *vp);就完成上面ab两个工作
//坑:
//云服务器不允许用户直接bind公网IP,另外,实际正常编写时候,我们也不会指名IP
//local.sin_addr.s_addr =inet_addr("ip");//点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
//INADDR_ANY:如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据
//才会交给你的网络进程,但是,一般服务器可能有多张网卡,配置多个ip,我们需要的不是
//某个ip上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
std::cerr<<"bind error:"<<errno<<std::endl;
return 2;
}
3.提供服务
//3.提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while (!quit)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
std::cout<<"client:"<<buffer<<std::endl;
std::string echo_hello = "hello";
sendto(sock,echo_hello.c_str(),echo_hello.size(),0,(struct sockaddr*)&peer,len);
}
4.编写client
#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
void Usage(std::string proc)
{
std::cout<<"Usage: \n\t" <<proc <<"server_ip server_port"<<std::endl;
}
// ./udp_client server_ip server_port
int main(int argc,char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 0;
}
//1、创建套接字,打开网络文件
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
std::cerr<<"socket create errno"<<errno<<std::endl;
return 1;
}
//客户端需要显示bind吗?
//a.首先,客户端必须也要有ip和port
//b.但是,客户端不需要显示bind!一旦显示bind,就必须明确,client要和哪一个port关联
//client指名的端口号,在client端一定会有吗?有可能被占用,被占用导致客户端无法使用
//server要的是port必须明确,而且不变,但client只要有就行!一般由OS自动给你bind()
//就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
// b.你要给谁发?
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//2、使用服务
while(1)
{
//a.你的数据从哪里来??
std::string message;
std::cout<<"请输入:"<<std::endl;
std::cin>>message;
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
//此处tmp就是一个”占位符“
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&tmp,&len);
std::cout<<"server echo# " <<buffer<<std::endl;
}
return 0;
}
如果你编写的udp无法通信,云服务器开放服务,首先需要开放端口,默认的云平台是没有开放特定的端口的!
需要所有者,在网页后端-》安全组-》开放端口
注意:
上述代码的结果是:实现一个通信功能的网络接口,双方可以在这些代码上进行网络通信
2.2.3改进:做一个小型的shell
udp_server.cc
#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
// const uint16_t port = 8080;
std::string Usage(std::string porc)
{
std::cout<<"Usage: "<< porc <<"port"<<std::endl;
}
//./udp_server port
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return -1;
}
uint16_t port = atoi(argv[1]);
//1.创建套接字,打开网络文件
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
std::cerr<<"socket create errno"<<errno<<std::endl;
return 1;
}
//2.给该服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);//此处的端口号,是我们计算机上的变量,是主机序列
//a.需要将人识别的点分十进制,字符串风格ip地址,转换成四字节整数IP
//b.也要考虑大小端
// in_addr_t inet_addr(const char *vp);就完成上面ab两个工作
//坑:
//云服务器不允许用户直接bind公网IP,另外,实际正常编写时候,我们也不会指名IP
//local.sin_addr.s_addr =inet_addr("公网ip");//点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
//INADDR_ANY:如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据
//才会交给你的网络进程,但是,一般服务器可能有多张网卡,配置多个ip,我们需要的不是
//某个ip上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!
local.sin_addr.s_addr = INADDR_ANY;
//明确服务器绑定的端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
std::cerr<<"bind error:"<<errno<<std::endl;
return 2;
}
//3.提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while (!quit)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//注意:我们默认认为通信的数据是双方在互发字符串。
ssize_t cnt = recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if (cnt > 0)
{
buffer[cnt] = 0;//0 == '\0'
FILE *fp = popen(buffer,"r");
std::string echo_hello = buffer;
char line[1014] = {0};
while (fgets(line,sizeof(line),fp) != NULL)
{
echo_hello+=line;
}
// if(feof(fp))//判断一个文件流是否被读到文件结尾
// {
// //读取结果完成
// }
pclose(fp);
std::cout << "client:" << buffer << std::endl;
//根据用户输入,构建一个新的返回字符串
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr *)&peer, len);
}
else{
//读取失败
}
}
return 0;
}
udp_client.cc
#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
void Usage(std::string proc)
{
std::cout<<"Usage: \n\t" <<proc <<"server_ip server_port"<<std::endl;
}
// ./udp_client server_ip server_port
int main(int argc,char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 0;
}
//1、创建套接字,打开网络文件
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
std::cerr<<"socket create errno"<<errno<<std::endl;
return 1;
}
//客户端需要显示bind吗?
//a.首先,客户端必须也要有ip和port
//b.但是,客户端不需要显示bind!一旦显示bind,就必须明确,client要和哪一个port关联
//client指名的端口号,在client端一定会有吗?有可能被占用,被占用导致客户端无法使用
//server要的是port必须明确,而且不变,但client只要有就行!一般由OS自动给你bind()
//就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
// b.你要给谁发?
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//2、使用服务
while(1)
{
//a.你的数据从哪里来??
//因为要读取空格,cin不支持,,,
// std::string message;
// std::cout<<"请输入:"<<std::endl;
// std::cin>>message;
std::cout<<"MyShell $";
char line[1024];
fgets(line,sizeof(line),stdin);
sendto(sock,line,strlen(line),0,(struct sockaddr*)&server,sizeof(server));
//此处tmp就是一个”占位符“
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
ssize_t cnt = recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&tmp,&len);
if(cnt>0)
{
//在网络通信中,只有报文大小,或者字节流中字节的个数,
//没有C/C++字符串这样的概念,虽然我们后续可能经常遇到类似的情况
buffer[cnt] = 0;
std::cout<<buffer<<std::endl;
}
else{
//读取出错
}
}
return 0;
}
Makefile
.PHONY:all
all::udp_server udp_client
udp_server:udp_server.cc
g++ -o $@ $^ -std=c++11
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf udp_client udp_server
2.3Tcp服务器的简单应用场景
1.创建套接字
2.绑定套接字
上述两个和udp一样
3.监听
补充:tcp是面向连接的,(在通信的时候必须要先建立连接)
举个栗子:
建立连接:(tcp)
打电话的时候,我需要先拨电话,你要接才可以正常通信,
即:在通信之前,我们多了一个步骤叫做:建立连接
不用建立连接:(udp )
你的朋友想要给你发快递,只 需要知道你的地址和电话,然后就可以直接发快递了
不需要给你打招呼,建立通信连接,发快递类似于udp
4.获取连接
因为tcp是面向字节流的,就如同文件一般,可以进行正常的读写
2.3.1单进程版本
tcp_server.cc
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "port" << std::endl;
}
// ./tap_serer 服务器端口号
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// tcp_server
// 1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);//和udp的区别,隐式套接字
if (listen_sock < 0) //创建失败
{
std::cerr << "socket error" << errno << std::endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind errno:" << errno << std::endl;
}
//3.因为tcp是面向连接的,
//a.在通信前,需要建立链接 b.然后才能通信
//一定有人主动建立(客户端,需要服务),一定有人被动接受链接(服务器,提供服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//
//设置套接字是Listen状态,本质是允许用户连接
//3.监听
const int back_log = 5;
if(listen(listen_sock,back_log)<0)
{
std::cerr<<"listen errno"<<std::endl;
return 4;
}
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if(new_sock<0)//获取新连接失败
{
continue;
}
std::cout<<"get a new link..."<<std::endl;
//version 1:单进程版,没人使用!
//提供服务
while(true)
{
char buffer[1024];
memset(buffer,0,sizeof(buffer));
ssize_t s = read(new_sock,buffer,sizeof(buffer));
if(s>0)//读取成功
{
buffer[s] = 0;//将获取的内容当成字符串
std::cout<<"client# "<< buffer << std::endl;
std::string echo_string = ">>>server<<<,";
echo_string += buffer;
write(new_sock,echo_string.c_str(),echo_string.size());
}
else if(s == 0)
{
std::cout<<"client quit..."<< std::endl;
break;
}
else
{
std::cerr << "read error"<<std::endl;
break;
}
}
}
return 0;
}
2.3.2多进程版本
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "port" << std::endl;
}
void ServiceIO(int new_sock)
{
//提供服务
while (true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer));
if (s > 0) //读取成功
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<,";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{
std::cout << "client quit..." << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
// ./tap_serer 服务器端口号
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// tcp_server
// 1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);//和udp的区别,隐式套接字
if (listen_sock < 0) //创建失败
{
std::cerr << "socket error" << errno << std::endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind errno:" << errno << std::endl;
}
//3.因为tcp是面向连接的,
//a.在通信前,需要建立链接 b.然后才能通信
//一定有人主动建立(客户端,需要服务),一定有人被动接受链接(服务器,提供服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//
//设置套接字是Listen状态,本质是允许用户连接
//3.监听
const int back_log = 5;
if(listen(listen_sock,back_log)<0)
{
std::cerr<<"listen errno"<<std::endl;
return 4;
}
signal(SIGCHLD,SIG_IGN);//在linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if(new_sock<0)//获取新连接失败
{
continue;
}
std::cout<<"get a new link...:"<<new_sock<<std::endl;
pid_t id = fork();
if(id < 0)
{
continue;
}
else if(id == 0)
{
//子进程
ServiceIO(new_sock);
exit(0);
}
else
{
//父进程
//do nothing!
}
}
return 0;
}
2.3.3修改补充多进程版本
如果不关闭不需要的文件描述符,会造成文件描述符泄露!
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "port" << std::endl;
}
void ServiceIO(int new_sock)
{
//提供服务
while (true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer));
if (s > 0) //读取成功
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<,";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{
std::cout << "client quit..." << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
// ./tap_serer 服务器端口号
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// tcp_server
// 1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);//和udp的区别,隐式套接字
if (listen_sock < 0) //创建失败
{
std::cerr << "socket error" << errno << std::endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind errno:" << errno << std::endl;
}
//3.因为tcp是面向连接的,
//a.在通信前,需要建立链接 b.然后才能通信
//一定有人主动建立(客户端,需要服务),一定有人被动接受链接(服务器,提供服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//
//设置套接字是Listen状态,本质是允许用户连接
//3.监听
const int back_log = 5;
if(listen(listen_sock,back_log)<0)
{
std::cerr<<"listen errno"<<std::endl;
return 4;
}
//signal(SIGCHLD,SIG_IGN);//在linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if(new_sock<0)//获取新连接失败
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port);
std::string cli_ip = inet_ntoa(peer.sin_addr);
std::cout<<"get a new link->:["<<cli_ip<<":"<<cli_port<<"] # "<<new_sock<<std::endl;
pid_t id = fork();
if(id < 0)
{
continue;
}
else if(id == 0)//曾经被父进程打开的fd,是否会被子进程继承呢?会,
{
//子进程
close(listen_sock);//无论父子进程中的哪一个,强烈建议关闭掉不需要的fd
if(fork()>0)//退出的是子进程
{
exit(0);
}
//向后走的进程,其实是孙子进程
ServiceIO(new_sock);
close(new_sock);
exit(0);
}
else
{
//父进程
waitpid(id,nullptr,0);//这里等待的时候,不会被阻塞!
close(new_sock);
}
}
return 0;
}
2.3.4使用线程
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "port" << std::endl;
}
void ServiceIO(int new_sock)
{
//提供服务
while (true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer));
if (s > 0) //读取成功
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<,";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{
std::cout << "client quit..." << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
void* HandlerRequest(void *args)
{
pthread_detach(pthread_self());
int sock = *(int*)args;
delete (int*)args;
ServiceIO(sock);
close(sock);
}
// ./tap_serer 服务器端口号
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// tcp_server
// 1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);//和udp的区别,隐式套接字
if (listen_sock < 0) //创建失败
{
std::cerr << "socket error" << errno << std::endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind errno:" << errno << std::endl;
}
//3.因为tcp是面向连接的,
//a.在通信前,需要建立链接 b.然后才能通信
//一定有人主动建立(客户端,需要服务),一定有人被动接受链接(服务器,提供服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//
//设置套接字是Listen状态,本质是允许用户连接
//3.监听
const int back_log = 5;
if(listen(listen_sock,back_log)<0)
{
std::cerr<<"listen errno"<<std::endl;
return 4;
}
//signal(SIGCHLD,SIG_IGN);//在linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if(new_sock<0)//获取新连接失败
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port);
std::string cli_ip = inet_ntoa(peer.sin_addr);
std::cout<<"get a new link->:["<<cli_ip<<":"<<cli_port<<"] # "<<new_sock<<std::endl;
//version 3:使用线程,曾经被主线程打开的fd,新线程是否能看到,是否共享?
pthread_t tid;
int *pram = new int(new_sock);
pthread_create(&tid, nullptr, HandlerRequest,pram);
}
return 0;
}
version2,3问题:
1.创建线程,进程无上限
进程和线程越多,不代表你的效率越高,由服务是主要的消耗,转化成为了进程切换是主要的消耗,一旦被切换出去,就需要等待大量时间才能切换会这个线程,切换成本变得更高,
a.线程或进程越来越多,会导致系统运行缓慢,从而导致服务器无法对外正常服务
2.当客户链接来了,我们才给客户提供创建进程/线程
2.3.5单例模式线程池
tcp_server.cc
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Task.hpp"
#include "thread_pool.hpp"
using namespace ns_threadpool;
using namespace ns_task;
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "port" << std::endl;
}
// ./tap_serer 服务器端口号
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// tcp_server
// 1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);//和udp的区别,隐式套接字
if (listen_sock < 0) //创建失败
{
std::cerr << "socket error" << errno << std::endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind errno:" << errno << std::endl;
}
//3.因为tcp是面向连接的,
//a.在通信前,需要建立链接 b.然后才能通信
//一定有人主动建立(客户端,需要服务),一定有人被动接受链接(服务器,提供服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//
//设置套接字是Listen状态,本质是允许用户连接
//3.监听
const int back_log = 5;
if(listen(listen_sock,back_log)<0)
{
std::cerr<<"listen errno"<<std::endl;
return 4;
}
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if(new_sock<0)//获取新连接失败
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port);
std::string cli_ip = inet_ntoa(peer.sin_addr);
std::cout<<"get a new link->:["<<cli_ip<<":"<<cli_port<<"] # "<<new_sock<<std::endl;
//version 4:进程或线程池
//1.构建一个任务
Task t(new_sock);
//2.将任务push到后端的线程池即可
ThreadPool<Task>::GetInstance()->PushTask(t);
return 0;
}
tcp_client.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
void Usage(std::string proc)
{
std::cout<<"Usage: "<<proc<<"server_ip server_port"<<std::endl;
}
// ./udp_client server_ip server_port
int main(int argc,char *argv[])
{
if(argc!= 3)
{
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1.创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0)//创建套接字失败
{
std::cerr<<"socket errno"<<std::endl;
return 2;
}
//client无需显示的bind,client->server
//client->connect!
struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
//该函数inet_addr:
//1.将点分十进制的字符串风格的ip,转化成为4字节的IP
//2.将4字节由主机转化为网络序列
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
server.sin_port = htons(svr_port);
//2.发起链接
if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
{
std::cout<<"connect server failed!"<<std::endl;
return 3;
}
std::cout<<"connet success!"<<std::endl;
//进行正常的业务请求
while (true)
{
std::cout<<"please Enter# ";
char buffer[1024];
fgets(buffer,sizeof(buffer)-1,stdin);
write(sock,buffer,strlen(buffer));
ssize_t s = read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s] = 0;
std::cout<<"server echo# "<<buffer<<std::endl;
}
}
return 0;
}
thread_pool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool
{
private:
int num_;
std::queue<T> task_queue_; //该成员是一个临界资源
pthread_mutex_t mtx_;
pthread_cond_t cond_;
static ThreadPool<T> *ins;
//构造函数必须要实现,但是必须需要私有化
//将构造函数放进私有,这个就不能定义对象
ThreadPool(int num = g_num) : num_(num)
{
pthread_mutex_init(&mtx_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
//不让这个线程池进行拷贝构造或者赋值,不让他进行写实拷贝
ThreadPool(const ThreadPool<T> &tp) = delete;
//赋值语句
ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
public:
static ThreadPool<T> *GetInstance()
{
//这里会被多个线程同时进入,所以需要加锁
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
if (ins == nullptr)//双判断,减少锁的争用,提高获取单例的效率!
{
pthread_mutex_lock(&lock);
//当前单例对象还没有被创建
if (ins == nullptr)
{
ins = new ThreadPool<T>();
ins->InitThreadPool();
std::cout << "首次加载对象" << std::endl;
}
pthread_mutex_unlock(&lock);
}
return ins;
}
void Lock()
{
pthread_mutex_lock(&mtx_);
}
void Unlock()
{
pthread_mutex_unlock(&mtx_);
}
void Wait()
{
pthread_cond_wait(&cond_, &mtx_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
bool IsEmpey()
{
return task_queue_.empty();
}
public:
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpey())
{
tp->Wait();
}
//此时任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t.Run();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < num_; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this /*?*/);
}
}
void PushTask(const T &in)
{
Lock();
task_queue_.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&cond_);
}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
Task.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h>
namespace ns_task
{
class Task
{
private:
int _sock;
public:
Task()
: _sock(-1)
{}
Task(int sock)
: _sock(sock)
{}
int Run()
{
//提供服务
// while (true)
// {
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(_sock, buffer, sizeof(buffer));
if (s > 0) //读取成功
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<,";
echo_string += buffer;
write(_sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{
std::cout << "client quit..." << std::endl;
// break;
}
else
{
std::cerr << "read error" << std::endl;
// break;
}
close(_sock);
//}
}
~Task() {}
};
}
Makefile
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11
tcp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf tcp_client tcp_server
3.TCP协议通讯流程
1.创建socket的过程,socket(),本质是打开文件----仅仅有系统相关的内容
2.bind(),struct sockaddr_in,
我们填充ip,port,本质是ip+port和文件信息进行关联
3.listen(),本质是设置socket文件的状态,允许别人来链接我
4.accept(),获取新链接到应用层,是以fd为代表的
新链接:当有很多个连接连上我们的服务器的时候,OS中会存在大量的连接呢?是的
OS要不要管理这些已经建立好的连接呢?当然
该如何管理?先描述,再组织
所谓的连接,在操作系统层面,本质上其实就是一个描述连接的结构体(文件)
5.read/write,本质,就是进行网络通信,但是,对于用户来讲,相当于我们在进行正常的文件读写
6.close(fd),关闭文件,
a.系统层面,释放曾经申请的文件资源
b.网络层面,通知对方,我的连接已经关闭了!
7.connect(),本质是发起链接,在系统层面,就是构建一个请求报文发送过去。
在网络层面,发起tcp链接的三次握手
8.close(),client&&server,本质在网络层面,其实就是进行四次挥手!