TCP协议:传输控制协议
特性:面向连接,可靠传输,面向字节流
应用场景:安全性要求大于实时性要求的场景---文件传输
1.创建套接字
int socket(int domain, int type, int protocol);
2.绑定地址
int bind(int sockfd, struct sockaddr* addr, socklen_t len);
3.开始监听
int listen(int sockfd, int backlog);
backlog:服务器能够在同一时间处理的最大连接数
4.客户端发送连接请求
int connect(int sockfd, struct sockaddr* srvaddr, socklen_t len);
sockfd:套接字描述符
srvaddr:服务端地址信息
len:地址长度
返回值:成功返回0,失败返回-1
5.服务端获取新建连接
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
sockfd:监听套接字,服务端最早创建的套接字,只用于接受新的连接请求
cliaddr:新的连接的客户端地址信息
addrlen:输入输出参数,指定地址信息长度,以及返回实际长度
返回值:新建连接的描述符,往后与客户端的通信都通过这个描述符完成
6.收发数据:tcp通信因为socket结构体中包含完整五元组,因此不需要指定地址
ssize_t send(int sockfd, void* data, int len, int flag);
sockfd:新建套接字描述符
data:要发送的数据
len:数据长度
flag:默认0--阻塞发送
返回值:成功返回实际发送的数据长度;失败返回1,连接断开会触发异常
ssize_t recv(int sockfd, void* buf, int len, int flag);
sockfd:新建套接字描述符
buf:接收数据的空间首地址
len:要接收的数据长度
flag:默认0--阻塞接收
返回值:成功返回实际接收的数据长度;出错返回-1;连接断开返回0
7.关闭套接字
int close(int sockfd);
#include
using namespace std;
#include
#include
#include
#define BACKLOG_MAX 5
#define CHECK(p) if(p==false) {return -1;}
class TcpSocket
{
public:
TcpSocket()
:_sockfd(-1)
{}
//创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0)
{
perror("create socket error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const string& ip, const uint16_t& port)
{
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, (sockaddr*)&addr, len);
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
//开始监听
bool Listen(int backlog = BACKLOG_MAX)
{
int ret = listen(_sockfd, backlog);
if(ret < 0)
{
perror("Listen error");
return false;
}
return true;
}
//客户端发送连接请求
bool Connect(const string& ip, const uint16_t& port)
{
sockaddr_in seraddr;//服务端地址信息
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(sockaddr_in);
int ret = connect(_sockfd, (sockaddr*)&seraddr, len);
if(ret < 0)
{
perror("connect failed");
return false;
}
return true;
}
//服务端获取新建连接
bool Accept(TcpSocket& tcpsocket, string* ip = NULL, uint16_t* port = NULL)
{
sockaddr_in cliaddr;
socklen_t len = sizeof(sockaddr_in);
int ret = accept(_sockfd, (sockaddr*)&cliaddr, &len);
if(ret < 0)
{
perror("accept error");
return false;
}
tcpsocket._sockfd = ret;
if(ip != NULL)
*ip = inet_ntoa(cliaddr.sin_addr);
if(port != NULL)
*port = ntohs(cliaddr.sin_port);
return true;
}
//发送数据
//要发送整条数据
bool Send(string& buf)
{
int total = 0;
while(total < buf.size())
{
int ret = send(_sockfd, buf.c_str() + total, buf.size() - total, 0);
if(ret < 0)
{
perror("send data failed");
return false;
}
total += ret;
}
return true;
}
//接收数据
bool Recv(string& buf)
{
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4095, 0);
if(ret < 0)
{
perror("recv error");
return false;
}
else if(ret == 0)
{
cout << "connect is broken" << endl;
return false;
}
buf.assign(tmp, ret);
return true;
}
//关闭套接字
bool Close()
{
if(_sockfd != -1)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
int _sockfd;
};
1.创建套接字 socket()
2.为套接字绑定地址信息(不推荐主动绑定) bind()
3.向服务端发起连接请求 connect()
4.发送数据 send()
5.接收数据 recv()
6.关闭套接字 close()
#include"tcp.hpp"
int main()
{
TcpSocket cli_socket;
//创建套接字
CHECK(cli_socket.Socket());
//绑定地址信息(不推荐)
//向服务器发送连接请求
CHECK(cli_socket.Connect("192.168.134.141", 9000));
while(1)
{
//发送数据
string buf;
cout << "client say:" ;
getline(cin, buf);
//发送数据失败,直接结束通信
CHECK(cli_socket.Send(buf));
buf.clear();
//接收数据
//接收数据失败,直接结束通信
CHECK(cli_socket.Recv(buf));
cout << "serve say:" << buf << endl;
}
//关闭套接字
cli_socket.Close();
return 0;
}
1.创建套接字 socket()
2.为套接字绑定地址信息 bind()
3.开始监听 listen()
4.获取新建连接 accept()
5.接收数据 recv()
6.发送数据 send()
7.关闭套接字 close()
#include"tcp.hpp"
int main()
{
TcpSocket lst_socket;
//创建套接字
CHECK(lst_socket.Socket());
//绑定地址信息
CHECK(lst_socket.Bind("192.168.134.141", 9000));
//开始监听
CHECK(lst_socket.Listen());
while(1)
{
//获取新建连接
TcpSocket newSocket;
string ip;
uint16_t port;
bool ret = lst_socket.Accept(newSocket, &ip, &port);
if(ret == false)
continue;
cout << "new connect ip:" << ip << " port:" << port << endl;
//接收数据
string buf;
ret = newSocket.Recv(buf);
if(ret == false)
{
newSocket.Close();
continue;
}
cout << "client say:" << buf << endl;
buf.clear();
//发送数据
cout << "server say:";
getline(cin, buf);
ret = newSocket.Send(buf);
if(ret == false)
{
newSocket.Close();
}
}
//关闭套接字
lst_socket.Close();
return 0;
}
测试结果:
根据测试结果来看,当客户端第一次发送数据给服务端时,服务端可以接收数据,并且回复客户端,但是客户端第二次发送数据给服务端时,服务端就收不到客户端的数据,也不能进行回复,这是为什么?
我们先来看下代码中服务端流程:
accept是阻塞接口,当服务器完成一次接收与发送数据,就会继续到获取新连接接口,此时若没有新的连接,就会一直等待,直到有新的连接到来。所以上面所出现的问题就是因为没有新的连接到来,进入阻塞状态。
recv与send都是阻塞接口,若没有接收到数据或者没有数据发送,就会一直等待,直到能接收到数据或者有数据可以发送。所以,它们三个接口,任意一个接口的调用,都有可能导致服务端流程阻塞。
如果服务端流程阻塞,那么其他客户端也不能进行正常的收发数据,为了解决这个问题,我们提出了多执行流并发处理,为每个客户端都创建一个执行流负责与这个客户端进行通信,这样做的好处就是:
1.主线程卡在获取新建连接这里,但是不影响客户端的通信
2.某个客户端的通信阻塞,也不会影响主线程以及其他线程
实际在代码中的处理:在主线程中,获取新建连接,一旦获取到了则创建一个执行流,通过这个新建连接与客户端进行通信。
两种具体的解决方法:多线程、多进程。
#include"tcp.hpp"
#include
void* pthread_comm(void* newSocket)
{
TcpSocket* Socket = (TcpSocket*)newSocket;
while(1)
{
bool ret;
//接收数据
string buf;
ret = Socket->Recv(buf);
if(ret == false)
{
Socket->Close();
delete Socket;
return NULL;
}
cout << "client say:" << buf << endl;
buf.clear();
//发送数据
cout << "server say:";
getline(cin, buf);
ret = Socket->Send(buf);
if(ret == false)
{
Socket->Close();
delete Socket;
return NULL;
}
}
Socket->Close();
delete Socket;
return NULL;
}
int main()
{
TcpSocket lst_socket;
//创建套接字
CHECK(lst_socket.Socket());
//绑定地址信息
CHECK(lst_socket.Bind("192.168.134.141", 9000));
//开始监听
CHECK(lst_socket.Listen());
while(1)
{
//获取新建连接
TcpSocket* newSocket = new TcpSocket;
string ip;
uint16_t port;
bool ret = lst_socket.Accept(*newSocket, &ip, &port);
if(ret == false)
continue;
cout << "new connect-->ip:" << ip << " port:" << port << endl;
//获取新建连接之后,创建线程负责与客户端进行通信
pthread_t tid;
int res = pthread_create(&tid, NULL, pthread_comm, (void*)newSocket);
if(res != 0)
{
perror("create pthread failed");
continue;
}
//线程分离
pthread_detach(tid);
}
//关闭套接字
lst_socket.Close();
return 0;
}
注意:
普通线程与主线程数据共享,主线程不能随意释放套接字,一旦释放其他线程无法使用。
#include"tcp.hpp"
#include
#include
void work(TcpSocket& newSocket)
{
//接收数据
while(1)
{
bool ret;
string buf;
ret = newSocket.Recv(buf);
if(ret == false)
{
//获取数据失败,关闭通信套接字,退出子进程
newSocket.Close();
exit(0);
}
cout << "client say:" << buf << endl;
buf.clear();
//发送数据
cout << "server say:";
getline(cin, buf);
ret = newSocket.Send(buf);
if(ret == false)
{
// 发送数据失败,关闭通信套接字,退出子进程
newSocket.Close();
exit(0);
}
}
newSocket.Close();
return;
}
int main()
{
signal(SIGCHLD, SIG_IGN);//信号函数,如果有子进程退出,忽略它的退出原因
TcpSocket lst_socket;
//创建套接字
CHECK(lst_socket.Socket());
//绑定地址信息
CHECK(lst_socket.Bind("192.168.134.141", 9000));
//开始监听
CHECK(lst_socket.Listen());
while(1)
{
//获取新建连接
TcpSocket newSocket;
string ip;
uint16_t port;
bool ret = lst_socket.Accept(newSocket, &ip, &port);
if(ret == false)
continue;
cout << "new connect ip:" << ip << " port:" << port << endl;
//创建子进程,子进程与客户端进行通信
pid_t pid = fork();
if(pid < 0)
{
perror("create fork failed");
//创建子进程失败,关闭的是父进程的通信套接字
newSocket.Close();
continue;
}
else if(pid == 0) //子进程
{
work(newSocket);
}
//父进程
//子进程用来通信,父进程的通信套接字就可以关闭了,防止资源泄漏
//父子进程数据独有,父进程关闭不会对子进程造成影响
newSocket.Close();
}
//关闭套接字
lst_socket.Close();
return 0;
}
注意:
父子进程数据各自独有,父进程用不到新建套接字因此创建子进程之后直接释放掉,否则会造成资源泄漏。同时也要注意僵尸进程的问题,我们在这里采用信号的方式,一旦有子进程退出的信号,我们就忽略处理,不去在意子进程的退出原因。