三次握手协议
基于TCP协议的网络编程的特点是 : 面向连接, 可靠传输, 提供字节流服务, 由于TCP协议的特性, 因此哎客户端与服务端进行数据通信时, 需要两端先建立连接
建立连接时 , 首先由客户端向服务端发起建立连接的请求, 确定服务端是否在线, 服务端在线的话就会对客户端做出回复, 但是为了避免客户端发起请求后进下线, 因此服务端在进行回复的信息中也会加上向客户端的请求,确定客户端是否能在线, 最后客户端接受到服务端的回复与请求信息之后,在对服务端做出回复--------这就是客户端与服务端进行连接的三次握手协议
在三次握手协议中, 我们将发起的请求称为SYN,将回复称为ACK
- 接口
- 对于服务端而言, 他在绑定问地址与端口之后, 会有一个状态为监听状态, 监听状态就是服务端等待客户端发起链接的请求
int listen(int sockfd, int backlog);
sockfd : 传入的套接字描述符
backlog : backlog指的是在sockfd所指向的结构体中的挂起链接队列中的最大数目
当服务端开始监听,并且客户端已经向服务端发器请求时,在服务端的套接字结构体中,就有两个队列, 一个是未完成的链接队列, 里面存放着正在建立连接的客户端的请求信息, 另一个是已经完成链接的队列,而backlog就描述的是已经完成链接的队列的最大个数,在进行数据通信的时候,服务端会从以完成链接队列中获取客户端信息进行通信
返回值 : 成功返回0,失败返回-1
- 对于客户端而言, 往往都是先进行数据的发送, 因此需要主动发起连接请求
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
connect接口就是用于客户端向服务端发起三次握手链接的请求的接口, 其中的参数都是描述服务端的信息.
返回值 : 成功返回0,失败返回-1
- 在建立好链接之后, 客户端与服务端在进行数据通信之前,服务端需要先从已完成链接队列中获取待客户端的结点, accept就是用来获取的接口
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值 : 对于服务端而言, 一开始绑定的套接字的作用在于,让客户端能向其发起连接请求 , 他并不会执行客户端与服务端之间的数据通信, 所以在服务端监听的时候,当客户端发送过来请求信息时, 一开始的套接字就会我发送过来的客户端信息创建出一个新的套接字, 并且保存在已完成链接队列中, 而accept接口在获取到队列中的信息记性通信的时候, 其实是新的套接字在进行数据的通信, 而accept的返回值就是新的套接字的描述符
总结而言 : 一开始创建的套接字只用于与多个客户端之间建立连接, 而新创建的套接字才是与客户端之间进行数据通信的真正本质
封装TcpSocket类
class TcpSocket{
public:
//构造函数
TcpSocket()
:_sockfd(-1)
{}
//析构函数
~TcpSocket(){
Close();
}
//套节接字初始化
bool SockInit(){
_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(_sockfd<0){
cout<<"创建套接字失败!!"<<endl;
return false;
}
return true;
}
//绑定地址
bool Bind(string& ip,uint16_t port){
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip.c_str());
socklen_t len=sizeof(addr);
int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0){
cout<<"绑定地址失败!!"<<endl;
return false;
}
return true;
}
//监听
bool Listen(int backlog=5){
int ret=listen(_sockfd,backlog);
if(ret<0){
cout<<"服务端监听失败!!"<<endl;
return false;
}
return true;
}
//发送链接请求
bool Connect(string& ip,uint16_t port){
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip.c_str());
socklen_t len=sizeof(addr);
int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0){
cout<<"发送链接请求失败!!"<<endl;
return false;
}
return true;
}
//获取新连接
void Setfd(int fd){
_sockfd=fd;
}
//获取已完成的新连接
bool Accept(TcpSocket& newsock){
struct sockaddr_in addr;
socklen_t len=sizeof(addr);
//accept的返回值为获取到的新的套接字的描述符
int newfd=accept(_sockfd,(struct sockaddr*)&addr, &len);
if(newfd<0){
cout<<"获取新连接失败!!"<<endl;
return false;
}
newsock.Setfd(newfd);
return true;
}
//发送数据
bool Send(string& buf){
int ret=send(_sockfd,&buf[0],buf.size(),0);
if(ret<0){
cout<<"接受数据失败"<<endl;
return false;
}
return true;
}
//接受数据
bool Recv(string& buf){
char tmp[4096]={0};
int ret=recv(_sockfd,tmp,4096,0);
if(ret<0){
cout<<"接受数据失败"<<endl;
return false;
}else if(ret==0){
cout<<"链接断开"<<endl;
return false;
}
//将tmp中的ret个字节色数据拷贝到buf中
buf.assign(tmp,ret);
return true;
}
//关闭套接字
bool Closr(){
if(_sockfd>0){
colse(_sockfd);
_sockfd=-1;
}
return true;
}
private:
int _sockfd;
};
多进程服务端
由于急于TCP协议的服务端每次与一个客户端建立连接就会创建一个新的套接字, 为了避免程序卡死, 我们利用多个执行流来完成急于TCP协议的服务端, 我们一个让主进程来负责与客户端建立连接,再创建出子进程来完成与客户端的数据通信
#define CHECK_RET(q) if((q)==false){return -1;}
void gigcb(int signo){
while(waitpid(-1,NULL,WNOHANG)>0);
}
int main(int argc,char* argv[]){
if(argc!=3){
cout<<"./tcp_srv ip port"<<endl;
return -1;
}
signal(SIGCHLD,sigcb)
string ip=argv[1];
uint16_t port=argv[2];
TcpSocket sock;
CHECK_RET(sock.SocketInit());
CHECK_RET(sock.Bind(ip,port));
CHECK_RET(sock.Lisent());
TcpSocket newsock;
while(1){
bool ret=sock.Accept(newsock);
if(ret==false){
continue;
}
if(fock()==0){
while(1){
//接受数据
string buf;
ret=newsocket.Recv(buf);
if(ret==false){
newsocket.Close();
continue;
}
cout<<"客户端说:"<<buf<<endl;
//发送数据
buf.clear();
cin>>buf;
newsocket.Send(buf);
}
newsock.Close();
exit(0);
}
newsock.Close();
}
sock.Close();
return 0;
}
注意:
在上述代码中, 主线程只用来接受新的客户端信息,在接收到之后就立马创建新的进程,让子进程来执行后续的通信任务 , 通信结束后,关闭 newsock 退出进程, 由于父子进程拥有同样的资源 ( 创建子进程,拷贝父进程的PCB ), 因此子进程直接可以对newsock进行操作, 而父进程的newsock没有作用所以直接进行关闭
为了避免当没有客户端发起连接时,服务端的父进程退出后,使所有用于数据通信的服务端子进程称为僵尸进程, 则对进程退出时返回的 SIGCHLD 信号进行处理, 使用waitpid()函数任意等待一个进程退出,直到所有进程退出为止
TCP的客户端程序与UDP服务端程序相差无几,只需在初始化玩套接字之后,添加一部向服务端发起连接请求即可, 具体代码参考文章—UDP套接字编程