概述
首先,我们知道,TCP是有连接的可靠的字节流协议。在基于TCP协议进行通信时,通信双方需要先建立一个TCP连接,建立连接需要进行三次握手,握手成功后才可以进行通信。
下图为典型的TCP客户端/服务器程序的函数调用。
网络通信流程
客户端流程
- 创建套接字。
- 不推荐手动绑定地址。
- (服务器开始监听之后)向服务器发起连接请求(向服务端发送SYN请求,服务端收到之后,恢复一个ACK同时向客户端发送一个SYN,客户端收到后,向服务器发送一个ACK)(三次握手)。
- 发送数据。
- 接收数据。
- 关闭套接字。
服务端流程
- 创建套接字。
- 绑定地址信息。
- 开始监听(告诉操作系统,可以接受客户端的连接请求了)。这个过程中,若有新的客户端连接请求到来,系统会为这个客户端新建一个socket提供一对一服务。第一次握手就会新建一个socket然后将这个socket放到未完成连接队列。完成三次握手后,再将socket放到已完成连接队列中。
- 获取已经连接成功的客户端socket,获取之后就可以通过这个新建的socket与指定客户端进行通信。
接口介绍
创建套接字:
int socket(int domain, int type, int protocol);
参数:
domin:地址域。
AF_INET:IPV4网络协议地址域。
AF_INET6:IPV6网络协议地址域。
type:套接字类型。
SOCK_STREAM:流式套接字,默认协议TCP,不支持UDP。
SOCK_DGRAM:数据报套接字,默认协议UDP,不支持TCP。
protocol:协议类型。
0:试用套接字默认协议。
6/IPPROTO_TCP:TCP协议。
17/IPPROTO_UDP:UDP协议。
返回值:套接字操作句柄(文件描述符),失败返回-1。
为套接字绑定地址信息:
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:创建套接字返回的描述符。
addr:地址信息。
addrlen:地址信息长度。
返回值:成功返回0,失败返回-1。
开始监听:
int listen(int sockfd, int backlog);
参数:
sockfd:套接字描述符。
backlog:已完成连接队列的最大节点数,决定了同一时间的最大并发连接数。
返回值:成功返回0,出错返回-1。
请求连接:
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符。
addr:发送端地址信息。
addrlen:地址信息长度。
返回值:成功返回0,出错返回-1。
获取已经连接成功的客户端:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:套接字描述符。
addr:发送端地址信息。
addrlen:地址信息长度。
返回值:返回新建的套接字的描述符,出错返回-1。
注意:该函数为阻塞获取已经完成的连接。
接收数据:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:操作句柄,套接字描述符。
buf:用buf存储接收的数据。
len:想要接收的数据长度。
flags:
0:默认阻塞接受。
返回值:大于0为实际接收的数据长度,小于0失败,等于0连接断开。
发送数据:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
socket:套接字描述符。
buf:要发送的数据。
len:要发送的数据长度。
flags:
0:默认阻塞发送。
返回值:实际发送的数据长度,失败返回-1。
关闭套接字:
int close(int fd);
参数:
fd:套接字描述符。
返回值:成功返回0,失败返回-1。
封装TcpSocket类
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <errno.h>
#include <string>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
using std::cout;
using std::endl;
class TcpSocket{
public:
TcpSocket()
: _sockfd(-1)
{}
void SetSockFd(int fd){
_sockfd = fd;
}
bool Socket(){
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0){
perror("socket error");
return false;
}
return true;
}
bool Bind(std::string& ip, uint16_t port){
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog = 10){
int ret = listen(_sockfd, backlog);
if(ret < 0){
perror("listen error");
return false;
}
return true;
}
bool Connect(std::string& ip, uint16_t port){
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("connect error");
return false;
}
return true;
}
bool Accept(TcpSocket& csock, struct sockaddr_in* addr = NULL){
struct sockaddr_in _addr;
socklen_t len = sizeof(struct sockaddr_in);
int new_sockfd = accept(_sockfd, (struct sockaddr*)&_addr, &len);
if(new_sockfd < 0){
perror("accept error");
return false;
}
if(addr != NULL){
memcpy(addr, &_addr, len);
}
csock.SetSockFd(new_sockfd);
return true;
}
bool Recv(std::string& buf){
char temp[1024] = {0};
int ret = recv(_sockfd, temp, 1024, 0);
if(ret < 0){
perror("recv error");
return false;
}
else if(ret == 0){
cout << "connect break!" << endl;
return false;
}
buf.assign(temp, ret);
return true;
}
bool Send(std::string& buf){
int ret = send(_sockfd, buf.c_str(), buf.size(), 0);
if(ret < 0){
perror("send error");
return false;
}
return true;
}
bool Close(){
int ret = close(_sockfd);
if(ret < 0){
perror("close error");
return false;
}
_sockfd = -1;
return true;
}
private:
int _sockfd;
};
TCP客户端和服务器的实现
TCP服务器
#include "tcp_socket.h"
int main(int argc, char* argv[]){
if(argc != 3){
cout << "./tcp_server ip port" << endl;
return -1;
}
bool ret;
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket sock;
ret = sock.Socket();
if(!ret){
return -1;
}
ret = sock.Bind(ip, port);
if(!ret){
return -1;
}
ret = sock.Listen();
if(!ret){
return -1;
}
while(1){
TcpSocket cli_sock;
struct sockaddr_in cli_addr;
if(!sock.Accept(cli_sock, &cli_addr)){
continue;
}
cout << "new connect client: " << inet_ntoa(cli_addr.sin_addr)
<< ", " << ntohs(cli_addr.sin_port) << endl;
std::string buf;
cli_sock.Recv(buf);
cout << "client say: " << buf.c_str() << endl;
buf.clear();
cout << "server say: ";
fflush(stdout);
std::cin >> buf;
cli_sock.Send(buf);
}
ret = sock.Close();
if(!ret){
return -1;
}
return 0;
}
TCP客户端
#include "tcp_socket.h"
int main(int argc, char* argv[]){
if(argc != 3){
cout << "./tcp_client ip port" << endl;
return -1;
}
bool ret;
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket sock;
ret = sock.Socket();
if(!ret){
return -1;
}
ret = sock.Connect(ip, port);
if(!ret){
return -1;
}
while(1){
std::string buf;
cout << "client say: ";
fflush(stdout);
std::cin >> buf;
sock.Send(buf);
buf.clear();
sock.Recv(buf);
cout << "server say: " << buf << endl;
}
ret = sock.Close();
if(!ret){
return -1;
}
return 0;
}
上面实现的客户端服务端的通信是有问题的,因为accept是阻塞获取已经完成的连接(recv也是阻塞接收),所以每个客户端只能和服务器完成一次会话,然后服务器就会阻塞等待新的客户端连接。从而整个会话被阻塞。
总结:因为accept是阻塞获取已经完成的连接而且recv也是阻塞获取数据,我么又不知道客户端连接请求什么时候来,客户端的数据什么时候发送,所以很容易造成程序的阻塞。可以使用多进程或者多线程解决这个问题。
TCP服务器(多进程)
#include <signal.h>
#include <sys/wait.h>
#include "tcp_socket.h"
struct sigaction act;
void myHandler(int signo){
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char* argv[]){
if(argc != 3){
cout << "./tcp_server_multiprocess ip port" << endl;
return -1;
}
act.sa_handler = myHandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
bool ret;
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket sock;
ret = sock.Socket();
if(!ret){
return -1;
}
ret = sock.Bind(ip, port);
if(!ret){
return -1;
}
ret = sock.Listen();
if(!ret){
return -1;
}
while(1){
TcpSocket cli_sock;
struct sockaddr_in cli_addr;
if(sock.Accept(cli_sock, &cli_addr) == false){
continue;
}
cout << "new connect client: " << inet_ntoa(cli_addr.sin_addr)
<< ", " << ntohs(cli_addr.sin_port) << endl;
pid_t pid = fork();
if(pid == 0){
while(1){
std::string buf;
cli_sock.Recv(buf);
cout << "client say: " << buf << endl;
buf.clear();
cout << "server say: ";
fflush(stdout);
std::cin >> buf;
cli_sock.Send(buf);
}
}
cli_sock.Close();
}
sock.Close();
return 0;
}
TCP服务器(多线程)
#include "tcp_socket.h"
#include <pthread.h>
void* thr_start(void* arg){
TcpSocket* sock = (TcpSocket*)arg;
while(1){
std::string buf;
sock->Recv(buf);
cout << "client say: " << buf << endl;
buf.clear();
cout << "server say: ";
fflush(stdout);
std::cin >> buf;
sock->Send(buf);
}
sock->Close();
delete sock;
return NULL;
}
int main(int argc, char* argv[]){
if(argc != 3){
cout << "./tcp_server_multithreading ip port" << endl;
return -1;
}
bool ret;
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket sock;
ret = sock.Socket();
if(!ret){
return -1;
}
ret = sock.Bind(ip, port);
if(!ret){
return -1;
}
ret = sock.Listen();
if(!ret){
return -1;
}
while(1){
TcpSocket* cli_sock = new TcpSocket;
struct sockaddr_in cli_addr;
if(!sock.Accept(*cli_sock, &cli_addr)){
continue;
}
cout << "new connect client: " << inet_ntoa(cli_addr.sin_addr)
<< ", " << ntohs(cli_addr.sin_port) << endl;
pthread_t tid;
pthread_create(&tid, NULL, thr_start, (void*)cli_sock);
pthread_detach(tid);
}
ret = sock.Close();
if(!ret){
return -1;
}
return 0;
}
如何判断TCP连接已经断开?
TCP的连接管理中,内建有保活机制。当长时间没有数据往来的时候,每隔一段时间都会向对方发送一个保活探测包,要求对方回复。当多次发送的保活探测包都没有响应时,则认为连接断开。
可通过命令查看sysctl -a | grep keepalive
。
连接断开,recv()函数会返回0,不是没有数据的意思,而是指连接断开。此时send()会触发异常(SIGPIPE)导致进程退出。
注意:TCP是面向字节流的协议,有可能接收到半条数据,因此一定要对返回值进行判断,判断数据是否已经接收或完全接收。