预备知识
理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
源IP地址: 表示该条信息来源于哪个机器。 目的IP地址: 表示该条信息去往于哪个进程
理解 “端口号” 和 “进程ID”
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用
- 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”
源端口号: 表示该条信息来源于哪个进程。 目的端口号: 表示该条信息去往于哪个机器。
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,后面再详细讨论
- 传输层协议
- 有连接: 双方在发送网络数据之前必须建立连接,再进行发送
- 可靠传输: 保证数据是可靠并且有序的到达对端,例如发送123、456时123数据先到达,456数据后到达。即使456数据先到达传输层,也会阻塞等待前面的数据123先到达。
- 面向字节流: TCP发送数据的单位是以字节为单位,并且数据没有明显的边界例如:123456数据不会分开
认识UDP协议
这里我们也先对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;,后面再详细讨论
- 传输层协议
- 无连接: 双方在发送网络数据之前不需要建立连接,直接发送,客服端不用管服务端是否在线
- 可靠传输: UDP并不会保证数据有序的到达对端
- 面向字节流: UDP不管向应用层还是网络层传递数据都是整条数据
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort)
uint32_t htonl(uint32_t hostlong)
uint16_t ntohs(uint16_t netshort)
uint32_t ntohl(uint32_t netlong)
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
UDP协议使用
socket编程接口
- socket常见API
int socket(int domain, int type, int protocol);
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
- sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX Domain Socket。然而, 各种网络协议的地址格式并不相同。
- 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 结构 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。 -
in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
简单的UDP网络程序
下面实现一个简单的网络通信程序
客户端文件 udp_client.cc
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstdio>
void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " desc_ip desc_port" << std::endl;
}
int main(int argc,char* argv[])
{
if( argc != 3 ){
Usage(argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
std::cerr<<"socket error"<<std::endl;
return 2;
}
char buffer[1024];
struct sockaddr_in desc;
memset(&desc,0,sizeof(desc));
desc.sin_family=AF_INET;
desc.sin_port = htons(atoi(argv[2]));
desc.sin_addr.s_addr = inet_addr(argv[1]);
for( ; ; ){
std::cout<<"Please Enter# "<< std::endl;
fflush(stdout);
buffer[0]=0;
ssize_t size=read(0,buffer,sizeof(buffer)-1);
if(size>0){
buffer[size-1]=0;
sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc,sizeof(desc));
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0){
buffer[s]=0;
std::cout<<"#echo "<<buffer<<std::endl;
}
}
}
close(sock);
return 0;
}
服务端文件 udp_server.cc
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<time.h>
#define PORT 8081
void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " local_port" << std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=2){
Usage(argv[0]);
return 1;
}
int sock=socket(AF_INET, SOCK_DGRAM, 0);
if(sock<0){
std::cerr<<"socket error"<<std::endl;
return 2;
}
std::cout<<"sock: "<<sock<<std::endl;
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 = htonl(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
std::cerr<<"bind error"<<std::endl;
return 1;
}
char message[1024];
for( ; ; ){
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len);
if(s>0){
message[s]='\0';
std::cout << "client# " << message << std::endl;
std::string echo_message = message;
echo_message += "_server_";
echo_message += std::to_string((long long)time(nullptr));
sendto(sock,echo_message.c_str(),echo_message.size(),0,(struct sockaddr*)&peer,len);
}
else{
}
}
close(sock);
return 0;
}
关于绑定的一些问题
Server端,为何要明确bind ? client:server = n:1, server给别人提供服务,就需要自己尽可能的将自己暴露出去(IP(域名)+PORT(一般是被隐藏的)),必须是“稳定”(不能轻易改变,尤其是端口号)的。
client为何不需要明确bind? 如果client没有port,也变无法与server进行通信。 为何不需要我们给他bind呢? 如果你自己bind了,成功了还好,如果你的client端口被别的程序占用,你的client就无法启动,客户端不是一定要用哪一个端口,只需要有一个端口就可以。我们一般不自己bind,而是由OS随机帮我们查找端口.
TCP协议使用
TCP socket API
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中
socket()
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符
- 应用程序可以像读写文件一样用read/write在网络上收发数据
- 如果socket()调用出错则返回-1
- 对于IPv4, family参数指定为AF_INET
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可
bind()
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号
- bind()成功返回0,失败返回-1
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号
- 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
我们的程序中对myaddr参数是这样初始化的
bzero ( &servaddr , sizeof ( servaddr ) ) ;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s _addr= hton1 (INADDR_ANY ) ;
servaddr.sin port = htons ( SERV_PORT);
- 将整个结构体清零
- 设置地址类型为AF_INET
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址
- 端口号为SERV_PORT
listen()
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究
- listen()成功返回0,失败返回-1
accept()
- 三次握手完成后, 服务器调用accept()接受连接
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号
- 如果给addr 参数传NULL,表示不关心客户端的地址
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
服务器程序结构是这样的
while ( 1 ){
cliaddr_len = sizeof( cliaddr ) ;
connfd =accept ( listenfd,(struct sockaddr * ) &cliaddr , &cliaddr_len ) ;
n = read ( connfd, buf,MAXLINE);
...
close( connfd ) ;
}
connect
- 客户端需要调用connect()连接服务器
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址
- connect()成功返回0,出错返回-1
查看tcp相关信息可以用如下指令 netstat -nltp
- n能显示数字
- l只查看listen状态的接口
- t查看tcp链接
- p查看到与tcp服务相关的进程信息
简单TCP网络程序
下面编写TCP网络程序实现通信
handler.hpp文件
#pragma once
#include"tcp_server.hpp"
namespace ns_handler{
using namespace ns_tcpserver;
#define SIZE 1024
void HandlerHelper(int sock)
{
while(true){
char buffer[1024];
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0){
buffer[s]=0;
std::cout<<"clinet# "<<buffer<<std::endl;
std::string echo_string =buffer;
if(echo_string=="quit"){
break;
}
echo_string +="[server say]";
write(sock,echo_string.c_str(),echo_string.size());
}
else if(s==0){
std::cout << sock << " : client quit ..." << std::endl;
break;
}
else{
std::cerr << "read error" << std::endl;
break;
}
}
}
void HandlerSock_V1(int sock)
{
HandlerHelper(sock);
}
}
tcp_client.hpp文件
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<strings.h>
#include<cstring>
namespace ns_tcpclient{
class TcpClient{
private:
std::string desc_ip;
uint16_t desc_port;
int sock;
public:
TcpClient(std::string _ip,uint16_t _port):desc_ip(_ip),desc_port(_port),sock(-1)
{}
void InitTcpClient()
{
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
std::cerr<<"socket error"<<std::endl;
exit(2);
}
}
void Start()
{
struct sockaddr_in svr;
bzero(&svr,sizeof(svr));
svr.sin_family= AF_INET;
svr.sin_port = htons(desc_port);
svr.sin_addr.s_addr = inet_addr(desc_ip.c_str());
if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))==0){
std::cout<<"connect success..."<<std::endl;
}
else{
std::cout<<"connect failed..."<<std::endl;
}
while(true){
char buffer[1024]={0};
std::cout << "请你输入# ";
fflush(stdout);
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0){
buffer[s-1]=0;
write(sock,buffer,strlen(buffer));
size_t rs=read(sock,buffer,sizeof(buffer)-1);
if(rs>0){
buffer[rs]=0;
std::cout<<buffer<<std::endl;
}
else{
std::cout<<"server close..."<<std::endl;
}
}
}
}
~TcpClient()
{
if(sock>=0) close(sock);
}
};
}
client.cc
#include "tcp_client.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " svr_ip svr_port" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
ns_tcpclient::TcpClient cli(ip, port);
cli.InitTcpClient();
cli.Start();
return 0;
}
tcp_server.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_tcpserver{
typedef void(*handler_t)(int);
const int backlog = 5;
class TcpServer{
private:
uint16_t port;
int listen_sock;
public:
TcpServer(int _port):port(_port), listen_sock(-1)
{}
void InitTcpServer()
{
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0){
std::cout << "socket error" << std::endl;
exit(2);
}
struct sockaddr_in local;
bzero(&local, 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);
}
}
void Loop(handler_t hander)
{
while(true){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0){
std::cout << "warning: accept error" << std::endl;
continue;
}
std::cout << "debug: sock->"<< sock << std::endl;
uint16_t peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
std::cout << "debug: " << peer_ip << ":" << peer_port << std::endl;
hander(sock);
close(sock);
}
}
~TcpServer()
{
if(listen_sock >= 0) close(listen_sock);
}
};
}
server.cc
#include"tcp_server.hpp"
#include"handler.hpp"
static void Usage(std::string proc)
{
std::cerr<<"Usage: "<<"\n\t"<<proc<<" prot"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=2){
Usage(argv[0]);
return 0;
}
uint16_t port=atoi(argv[1]);
ns_tcpserver::TcpServer* svr=new ns_tcpserver::TcpServer(port);
svr->InitTcpServer();
svr->Loop(ns_handler::HandlerSock_V1);
return 0;
}
上面的程序一次只能有一个客户端和服务端通信,能不能让服务端一次和多个客户端通信呢?这里可以利用多进程
void HandlerSock_V2(int sock)
{
if(fork()==0){
if(fork()>0){
exit(0);
}
HandlerHelper(sock);
exit(0);
}
close(sock);
waitpid(-1,nullptr,0);
}
这里利用了孙子进程,避免了子进程需要父进程等待的问题
进程创建的成本高,我们也可以创建线程
void* thread_routine(void* args)
{
int sock=*(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
HandlerHelper(sock);
close(sock);
return nullptr;
}
void HandlerSock_V3(int sock)
{
pthread_t tid;
int* p=new int(sock);
pthread_create(&tid,nullptr,thread_routine,p);
}
创建进程和线程的优缺点
多进程:链接来了,才创建进程,而且没有上限, 优点: 稳定,进程是具独立性的。缺点:进程创建和交互的成本高,效率变低。 多线程:链接来了,才创建线程,而且没有上限, 优点: 轻量化。缺点:健壮性不足(一个线程崩掉整个进程都会崩掉)一旦系统中的进程或者线程极度增多,进程或者线程在系统内切换的成本增加,切换的周期变长了,甚至导致系统宕机。
用TCP模拟英译汉服务器
tcp_server.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_tcpserver{
typedef void (*handler_t)(int);
const int backlog = 5;
class TcpServer{
private:
uint16_t port;
int listen_sock;
public:
TcpServer(int _port):port(_port), listen_sock(-1)
{}
void InitTcpServer()
{
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0){
std::cout << "socket error" << std::endl;
exit(2);
}
struct sockaddr_in local;
bzero(&local, 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);
}
}
void Loop(handler_t handler)
{
while(true){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0){
std::cout << "warning: accept error" << std::endl;
continue;
}
std::cout << "debug: sock->"<< sock << std::endl;
uint16_t peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
std::cout << "debug: " << peer_ip << ":" << peer_port << std::endl;
handler(sock);
}
}
~TcpServer()
{
if(listen_sock >= 0) close(listen_sock);
}
};
}
server.cc文件
#include "tcp_server.hpp"
#include "handler.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage:" << "\n\t" << proc << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2){
Usage(argv[0]);
return 0;
}
uint16_t port = atoi(argv[1]);
ns_tcpserver::TcpServer *svr = new ns_tcpserver::TcpServer(port);
svr->InitTcpServer();
svr->Loop(ns_handler::HandlerSock_V4);
return 0;
}
tcp_cilent.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
namespace ns_tcpclient{
class TcpClient{
private:
std::string desc_ip;
uint16_t desc_port;
int sock;
public:
TcpClient(std::string _ip, uint16_t _port):desc_ip(_ip), desc_port(_port), sock(-1)
{}
void InitTcpClient()
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
void Start()
{
struct sockaddr_in svr;
bzero(&svr, sizeof(svr));
svr.sin_family = AF_INET;
svr.sin_port = htons(desc_port);
svr.sin_addr.s_addr = inet_addr(desc_ip.c_str());
if(connect(sock, (struct sockaddr*)&svr, sizeof(svr)) == 0){
std::cout << "connect success ..." << std::endl;
}
else{
std::cout << "connect failed ..." << std::endl;
return;
}
while(true){
char buffer[1024] = {0};
std::cout << "请你输入# ";
fflush(stdout);
ssize_t s = read(0, buffer, sizeof(buffer)-1);
if(s > 0){
buffer[s-1] = 0;
write(sock, buffer, strlen(buffer));
ssize_t rs = read(sock, buffer, sizeof(buffer)-1);
if(rs > 0){
buffer[rs] = 0;
std::cout << buffer << std::endl;
}
else{
std::cout << "server close ..." << std::endl;
break;
}
}
}
}
~TcpClient()
{
if(sock >= 0) close(sock);
}
};
}
client.cc文件
#include "tcp_client.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " svr_ip svr_port" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
ns_tcpclient::TcpClient cli(ip, port);
cli.InitTcpClient();
cli.Start();
return 0;
}
thread_pool.hpp线程池文件
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
template <class T>
class ThreadPool{
private:
std::queue<T> q;
pthread_mutex_t lock;
pthread_cond_t cond;
private:
ThreadPool()
{
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
}
ThreadPool(const ThreadPool<T>&) = delete;
ThreadPool<T>& operator = (const ThreadPool<T>&) = delete;
static ThreadPool<T> *instance;
public:
static ThreadPool<T> *get_instance(int num)
{
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if(nullptr == instance){
pthread_mutex_lock(&mtx);
if(nullptr == instance){
instance = new ThreadPool<T>();
instance->InitThreadPool(num);
}
pthread_mutex_unlock(&mtx);
}
return instance;
}
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnlockQueue()
{
pthread_mutex_unlock(&lock);
}
bool IsEmpty()
{
return q.size() == 0;
}
void ThreadWait()
{
pthread_cond_wait(&cond, &lock);
}
void ThreadWakeup()
{
pthread_cond_signal(&cond);
}
void PopTask(T *out)
{
*out = q.front();
q.pop();
}
static void *Routinue(void *args)
{
pthread_detach(pthread_self());
ThreadPool *tp = (ThreadPool*)args;
while(true){
tp->LockQueue();
while(tp->IsEmpty()){
tp->ThreadWait();
}
T t;
tp->PopTask(&t);
tp->UnlockQueue();
t();
}
}
void InitThreadPool(int num)
{
for(auto i = 0; i < num; i++){
pthread_t tid;
pthread_create(&tid, nullptr, Routinue, this);
}
}
void PushTask(const T &in)
{
LockQueue();
q.push(in);
ThreadWakeup();
UnlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
};
template<class T>
ThreadPool<T>* ThreadPool<T>::instance = nullptr;
handler.hpp文件
#pragma once
#include "thread_pool.hpp"
#include "tcp_server.hpp"
#include <unistd.h>
#include <unordered_map>
#include <sys/wait.h>
#include <pthread.h>
namespace ns_handler{
using namespace ns_tcpserver;
#define SIZE 1024
std::unordered_map<std::string, std::string> dict = {
{"apple" , "苹果"},
{"hello", "你好"},
{"bit", "比特"},
{"banana", "香蕉"},
};
void HandlerHelper(int sock)
{
char buffer[1024];
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s>0){
buffer[s] = 0;
std::cout << "client say# " << buffer <<std::endl;
std::string k = buffer;
std::string message = "我不知道";
auto iter = dict.find(k);
if(iter != dict.end()){
message = iter->second;
}
write(sock, message.c_str(), message.size());
std::cout << "server echo# " << message <<std::endl;
}
}
void HandlerSock_V1(int sock)
{
HandlerHelper(sock);
}
void HandlerSock_V2(int sock)
{
if(fork() == 0){
if(fork() > 0){
exit(0);
}
HandlerHelper(sock);
exit(0);
}
waitpid(-1, nullptr, 0);
}
void *thread_routine(void *args)
{
int sock = *(int*)args;
delete (int*)args;
std::cout << "debug: " << sock << std::endl;
pthread_detach(pthread_self());
HandlerHelper(sock);
close(sock);
return nullptr;
}
void HandlerSock_V3(int sock)
{
pthread_t tid;
int *p = new int(sock);
pthread_create(&tid, nullptr, thread_routine, p);
}
class task{
private:
int sock;
public:
task(){}
task(int _sock):sock(_sock)
{}
void operator()()
{
std::cout << "当前处理的线程是:" << pthread_self() << std::endl;
HandlerHelper(sock);
close(sock);
}
~task(){}
};
void HandlerSock_V4(int sock)
{
ThreadPool<task>::get_instance(5)->PushTask(task(sock));
}
}
运行结果
TCP协议通讯流程
TCP协议通讯通讯的前提是先将客户端和服务端连接起来,这里的连接是指什么呢?
客户端连接服务器的时候,本质上是连接了服务器的操作系统(协议栈)模块。服务端和客户端是1:n的关系,所以一定会有多个客户端去连接一个服务器OS。 此时服务器OS上会有大量的客户端连接。服务器OS需要将所有连接管理起来(先描述,再组织)。 所谓的连接本质就是在双方OS内,维护对应的数据结构,建立了连接,后序也要维护连接,所以,建立连接是有成本的,消耗了时间和空间。
断开连接的本质:释放双方建立好的数据结构
下图是基于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段
TCP 和 UDP 对比
TCP是可靠传输,有连接,字节流 UDP是不可靠传输,无连接,数据报
|