前言
udp通信:无连接,不可靠,面向数据报
链接: udp通信总结.
tcp通信:面向连接,可靠传输,面向字节流
一.tcp通信流程
面向连接:必须建立了连接保证双方都具有数据收发能力,才开始通信;
(udp是只要知道对端地址就可以直接发送数据)
二.套接字接口
1.创建套接字
int socket(int domain,int type,int protocol)
- domain:地址域–确定本次socket通信使用哪种协议版本的地址结构–不同的协议版本有不同的地址结构–AF_INET IPV4
- type:SOCK_DGRAM-数据报套接字/SOCK_STREAM-流式套接字
- protocol:IPPROTO_TCP
2.绑定地址信息
int bind(int sockfd,struct sockaddr addr,socklen_t len)
3.开始监听
listen(int sockfd,int backlog)
- sockfd:将哪个套接字设置为监听状态。并且监听状态后可以开始接收客户端连接请求
- backlg:同一时间的并发连接数,决定同一时间最多接收多少个客户端的连接请求
已完成连接队列(completed connection queue)
(1)三次握手已经完成,但还未被应用层接收(accept),但也处于ESTABLISHED状态.
(2)队列长度由listen的backlog参数和内核的 net.core.somaxconn 参数共同决定.
(3)当这个队列满了之后,不管未完成连接队列是否已满,是否启用syncookie,都不在接收新的SYN请求.(该特性跟内核版本有关)
(4)如果client端向已完成连接队列的socket发送包,内核将保存数据到socket的接收缓冲区,等应用层accept之后,传给应用层.
未完成连接队列(incomplete connection queue)
(1)半连接状态,处于SEND_RCVD状态.
(2)由内核参数 net.ipv4.tcp_max_syn_backlog 设置.
(3)如果启用了syncookie,在未完成连接队列满了之后,新的SYN请求将使用syncookie机制.
服务器会重发SYN-ACK,重试次数为:/proc/sys/net/ipv4/tcp_synack_retries
4.获取新建连接(从已完成连接队列中取出一个socket,并且返回这个socket的描述符操作句柄)
int accept(int sockfd,struct sockaddr cli_addr,socklen_t len)
- sockfd:监听套接字,表示要获取哪个tcp服务端套接字的新建连接
- cli_addr:这个新的套接字对应的客户端地址信息
- len:地址信息长度
- 返回值:新建的socket套接字的描述符-----外部程序中的操作句柄
5.接收数据/发送数据(因为tcp通信套接字中已经标识了五元组,因此不需要接收数据的时候获取对方地址信息,发送数据的时候也不需要指定对方的地址信息)
- ssize_t recv(int sockfd,char *buf,int len,int flag);默认阻塞,没有数据则等待,连接断开返回0,不再阻塞
- ssize_t send(int sockfd,char *data,int len,int flag);默认阻塞,缓冲区数据满了则等待,连接断开则触发SIGPIPE异常
6.关闭套接字
int close(fd);
7.向服务端发起连接请求
int connect(int sockfd,struct sockaddr srv_addr,int len)
- srv_addr:服务端地址信息
- connect接口会描述对端地址信息
三.注意事项
解决方案:多线程/多进程----一个执行流只负责一件事,主线程/父进程只负责获取新连接,新连接到来accept获取新连接之后,新建执行流,让新的执行流专门与客户端通信;
多进程:主进程只负责获取新连接,有新连接获取成功,则创建子进程,子进程负责与客户端进行通信;要注意僵尸子进程处理;父进程记得要关闭不使用的描述符
多线程:主线程创建线程之后,不能关闭新建套接字,因为线程之间共享文件描述符表,描述符需要通过参数传递给普通线程
四.代码
tcpsocket.hpp
#include <cstdio>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
//listen的第二个参数决定同一时间能够接收多少客户端连接
//并不决定整体通信能够接收多少客户端连接
#define MAX_LISTEN 5
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket {
public:
TcpSocket ():_sockfd(-1){}
bool Socket() {
//socket(地址域, 套接字类型, 协议类型)
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0) {
perror("socket error");
return false;
}
return true;
}
bool Bind(const std::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(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 = MAX_LISTEN) {
//listen(套接字描述符, 最大并发连接数)
int ret = listen(_sockfd, backlog);
if (ret < 0) {
perror("listen error");
return false;
}
return true;
}
bool Accept(TcpSocket *new_sock, std::string *ip=NULL, uint16_t *port=NULL) {
//新建套接字描述符 = accept(监听套接字描述符, 客户端地址信息,地址长度);
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if (new_fd < 0) {
perror("accept error");
return false;
}
new_sock->_sockfd = new_fd;
if (ip != NULL) {
(*ip) = inet_ntoa(addr.sin_addr);
}
if (port != NULL) {
*port = ntohs(addr.sin_port);
}
return true;
}
bool Recv(std::string *buf) {
//recv(通信套接字描述符,缓冲区首地址,接收数据长度, 标志位-0阻塞)
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0) {
perror("recv error");
return false;
}else if (ret == 0) {//recv默认阻塞,没有数据就会等待,返回0,表示连接断开
printf("connection broken\n");
return false;
}
buf->assign(tmp, ret);
return true;
}
bool Send(const std::string &data) {
//send(描述符,要发送数据首地址,要发送的数据长度,标志位-0阻塞)
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if (ret < 0) {
perror("send error");
return false;
}
return true;
}
bool Close() {
if (_sockfd > 0) {
close(_sockfd);
_sockfd = -1;
}
return true;
}
bool Connect(const std::string &ip, uint16_t port) {
//向服务端发起连接
//connect(描述符, 服务端地址信息, 地址信息长度)
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(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0) {
perror("connect error");
return false;
}
return true;
}
private:
int _sockfd;
};
tcp_cli.cpp
#include <iostream>
#include "tcpsocket.hpp"
int main (int argc, char *argv[])
{
if (argc != 3) {
std::cout << "Usage: ./tcp_cli ip port\n";
return -1;
}
std::string srv_ip = argv[1];
uint16_t srv_port = std::stoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(srv_ip, srv_port));
while(1) {
std::string buf;
std::cout << "client say: ";
std::cin >> buf;
sock.Send(buf);
buf.clear();
sock.Recv(&buf);
std::cout << "server say: " << buf << std::endl;
}
sock.Close();
return 0;
}
tcp_process.cpp多进程版本
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
void sigcb(int no) {
//SIGCHLD信号是一个非可靠信号,有可能丢失
//因此在一次信号处理中,就需要处理到没有子进程退出为止
while(waitpid(-1, NULL, WNOHANG) > 0);//返回值大于0,表示有子进程退出
}
int main (int argc, char *argv[])
{
if (argc != 3) {
std::cout << "Usage: ./tcp_srv ip port\n";
return -1;
}
signal(SIGCHLD, sigcb);
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());//创建监听套接字
CHECK_RET(lst_sock.Bind(ip, port));//为监听套接字绑定地址
CHECK_RET(lst_sock.Listen());//开始监听
while(1) {
TcpSocket new_sock;
bool ret = lst_sock.Accept(&new_sock);//通过监听套接字获取新建连接
if (ret == false) {
continue;//服务端不能因为或一个新建套接字失败就退出
}
int pid = fork();//子进程复制父进程,父进程有的子进程都有
if (pid == 0) {
while(1) {
std::string buf;
new_sock.Recv(&buf);//通过新建连接与指定客户端进行通信
std::cout << "client say: " << buf << std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
new_sock.Send(buf);
}
new_sock.Close();//new_sock父子进程各有各的 ,父子进程数据独有
exit(0);
}
new_sock.Close();//父进程关闭自己的不使用的socket,不影响子进程
}
lst_sock.Close();
return 0;
}
tcp_thread.cpp多线程版本
#include <iostream>
#include <pthread.h>
#include "tcpsocket.hpp"
void *thr_worker(void *arg)
{
TcpSocket* new_sock = (TcpSocket*)arg;
while (1) {
std::string buf;
new_sock->Recv(&buf);//通过新建连接与指定客户端进行通信
std::cout << "client say: " << buf << std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
new_sock->Send(buf);
}
new_sock->Close();//线程之间文件描述符表共享,这边关闭了描述符其它地方就都用不了了
delete new_sock;
return NULL;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cout << "Usage: ./tcp_srv ip port\n";
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());//创建监听套接字
CHECK_RET(lst_sock.Bind(ip, port));//为监听套接字绑定地址
CHECK_RET(lst_sock.Listen());//开始监听
while (1) {
//TcpSocket new_sock;
TcpSocket *new_sock = new TcpSocket();
bool ret = lst_sock.Accept(new_sock);//通过监听套接字获取新建连接
if (ret == false) {
continue;//服务端不能因为或一个新建套接字失败就退出
}
pthread_t tid;
//new_sock是一个局部对象,循环完毕的时候就会被释放,传地址会造成错误
//除非每次accept的时候new_sock在堆上申请一个新的,不会自动释放,也要防止第二次获取时候覆盖上次的值
//或者直接传值在函数栈中会新建空间存储值与原数据空间就没关系了-仅限于数据占用空间比较小
int res = pthread_create(&tid, NULL, thr_worker, (void*)new_sock);
if (res != 0) {
std::cout << "pthread create error\n";
continue;
}
pthread_detach(tid);//不关心线程返回值,也不想等待释放资源,因此将线程分离出去
//new_sock.Close();//主线程不能关闭描述符,因为线程间描述符表是共享的
}
lst_sock.Close();
return 0;
}