TCP(传输控制协议)特点:面向链接,可靠传输,面向字节流。
应用于安全性要求大于实时性的场景,如文件传输
通信五元组:源端IP和port,对端IP和port,协议;(sip+sport+dip+dport+proto)
TCP通信流程
Server端:
-
创建套接字:
在内核中创建socket结构体 -
为套接字绑定地址信息—描述socket结构。
给创建的socket结构绑定源端IP和端口,协议
作用:发:1.发送数据时指定源端地址信息
收:2.告诉系统收到的哪条数据应该交给这个socket -
开始监听—将socket状态置为LISTEN状态(监听socket)
告诉系统可以开始处理客户端的链接请求。(先建立连接,后通信),处于LISTEN状态才会处理接到的数据,否则丢弃。
当服务端收到一个新建连接请求时会为这个客户端专门创建一个新的socket(通信socket),用于进行通信。通过这个socket知道通信双方的信息,所以不一定非得客户端先发数据。 -
获取到新建链接的描述符。
从已完成队列中取出socket -
收发数据
通过获取到的新建连接套接字与指定客户端通信。 -
关闭套接字
Client端:
- 创建套接字
- 绑定地址信息—>不推荐
- 向服务端发起连接请求
- 收发数据
- 关闭套接字
TCP通信接口认识:
服务端:
- 创建套接字
int socket(int domain, int type, int protocol);
domain:地址域类型。
AF_INET : ipv4地址域类型 struct sockaddr_in
AF_INET6: ipv6地址域类型 struct sockaddr_in6
AF_UNIX: 本地 地址域类型 struct sockaddr_un
type:套接字类型
SOCK_STREAM:流式套接字—--提供字节流传输 默认协议为TCP
SOCK_DGRAM:数据报套接字---提供数据包传输 默认协议为UDP
protocol:通信所使用的协议类型(常用TCP/UDP)
0:表示套接字类型默认协议
IPPROTO_TCP(0):TCP协议
IPPROTO_UDP(17):UDP协议
返回值:成功返回一个文件描述符—>操作句柄,失败返回-1
- 为套接字绑定地址信息
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockkfd: 创建套接字返回的描述符
addr: struct sockaddr 通用结构体
addrlen:指定地址结构长度
返回值:成功返回0,失败-1;
- 开始监听
int listen(int sockfd, int backlog);
sockfd:套接字描述符。
backlog:服务端同一时间能够建立的连接数量。(socket未完成连接的队列容量)
- 获取新建连接–获取新建链接的描述符
客户端向服务端发起连接请求:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
描述符 服务端地址 地址长度
服务端处理客户端的请求,获取新建连接:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
监听描述符 获取客户端地址 获取长度
这是个阻塞接口,当前没有新建连接会一直等待。
sockfd:监听套接字的描述符。
Addr:新建连接的客户端的地址信息
Addrlen:获取客户端的地址长度。
返回值:成功返回新建连接socket描述符—用于与指定客户端进行通信的句柄;失败 -1;
- 收发数据
发送和接受数据不需要远端对端地址,专用的通信套接字里面已经有地址信息:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
返回实际发送的长度,TCp发送字节流,可能发送成功但是数据没法送完。
Send发送数据时,如果连接已经断开,则触发异常。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
返回值: 1.返回实际获取到的数据长度
2.当连接断开时,返回0
3.出错返回-1
问题:默认情况下一个TCP服务器只能与一个客户端通信一次;
多执行流并发服务器的编写:多进程,多线程; 需要注意:获取新建连接之后创建执行流与指定客户端进行通信。多进程要防止出现僵尸进程,要考虑父子进程数据独有,父进程的新建连接要关掉。多线程中资源共享,一个线程关闭_sockfd其他线程则无法使用。
TCP服务端和客户端通信代码:
- 首先封装了一个tcp_socket接口类,方便调用这些接口操作。
tcp_socket.hpp:
#include <cstdio>
#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define MAX_LISTEN 5
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket{
public:
TcpSocket():_sockfd(-1)
{}
void Addr(sockaddr_in *addr,const std:: string &ip,uint16_t port){
addr->sin_family=AF_INET;
addr->sin_addr.s_addr=inet_addr(ip.c_str());
addr->sin_port = htons(port);
}
bool Socket(){
_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (_sockfd < 0) {
perror("sockert error");
return false;
}
return true;
}
bool Bind(const std:: string &ip,uint16_t port){
struct sockaddr_in addr;
Addr(&addr,ip,port);
int ret =bind(_sockfd,(sockaddr*)&addr,sizeof(struct sockaddr_in));
if (ret < 0) {
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog =MAX_LISTEN){
int ret= listen(_sockfd,backlog);
if(ret<0){
perror("listen error");
return false;
}
return true;
}
bool Connect(const std:: string &ip,uint16_t port){
struct sockaddr_in addr ;
Addr(&addr,ip,port);
int ret=connect(_sockfd,(sockaddr*)&addr,sizeof(struct sockaddr_in));
if(ret<0){
perror("connect error");
return false;
}
return true;
}
bool Accept(TcpSocket *sock, std:: string *ip=NULL,uint16_t* port=NULL){
struct sockaddr_in addr;
socklen_t len=sizeof(struct sockaddr_in);
int ret = accept(_sockfd,(sockaddr*)&addr,&len);
if(ret<0){
perror("accept error");
return false;
}
sock->_sockfd=ret;
if(ip != NULL) {
*ip = inet_ntoa(addr.sin_addr);
}
if (port != NULL) {
*port = ntohs(addr.sin_port);
}
return true;
}
bool Send(const std:: string &data){
int total=0;
while(total<data.size()){
ssize_t ret =send(_sockfd,&data[0+total],data.size()-total,0);
if(ret<0){
perror("send error");
return false;
}
total +=ret;
}
return true;
}
bool Recv(std:: string *buf){
char tmp[4096]={0};
int ret = recv(_sockfd,tmp,4096,0);
if(ret<0){
perror("recv error");
return false;
}
else if(ret==0){
printf("send shut down\n");
return false;
}
buf->assign(tmp,ret);
return true;
}
bool Close(){
if(_sockfd!=-1){
close(_sockfd);
}
return true;
}
private:
int _sockfd;
};
- 服务端:tcp_server.cpp
#include"tcp_socket.hpp"
int main(int argc,char*argv[])
{
//argc是程序运行参数的个数
//argv存放程序运行参数,以空格隔开,第0个参数为程序自己。
//eg: ./tcp_server 192.168.86.3 9000
if(argc!=3){//首先判断参数数量对着没
printf("usage: ./tcp_client srvip srvport\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 comm_sock;
bool ret =lst_sock.Accept(&comm_sock);
if(ret==false){
usleep(1000);
continue;
}
std::cout<<"new Connect"<<ip<<":"<<port<<":"<<std::endl;
std::string buf;
ret =comm_sock.Recv(&buf);
if(ret==false){
comm_sock.Close();
continue;
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"srv say: ";
std::cin>>buf;
ret=comm_sock.Send(buf);
if(ret==false){
comm_sock.Close();
continue;
}
}
lst_sock.Close();
return 0;
}
- 客户端:tcp_client.cpp
#include "tcp_socket.hpp"
int main(int argc,char*argv[]){
if(argc!=3){
printf("usage: ./tcp_client srvip srvport\n");
return -1;
}
std::string srv_ip=argv[1];
uint16_t srv_port=std::stoi(argv[2]);
TcpSocket cli_sock;
CHECK_RET(cli_sock.Socket());
CHECK_RET(cli_sock.Connect(srv_ip, srv_port));
while(1){
std::string buf;
std::cout << "client say:";
std::cin >> buf;
CHECK_RET(cli_sock.Send(buf));
buf.clear();
CHECK_RET(cli_sock.Recv(&buf));
std::cout << "server say:" << buf << std::endl;
}
cli_sock.Close();
return 0;
}
结果演示:
可以发现每个新建连接只能通话一次,然后链接就被accept()函数(获取新建连接)阻塞了,其实数据已经发送到接收缓冲区了。
其中服务端处理数据的流程为:
while(1){
accept()---获取新建连接
recv()--通过新建连接获取客户端发送的数据
send()通过新建连接向客户端发生数据
}
其中accept()和recv()都是阻塞接口
当前业务流程中,我们无法获知有没有新客户端到来,以及哪个客户端有数据到来,程序可能卡在accpet或recv的任意一处(因为当前程序流程是固定的,后面可以使用多路转接模型来解决)。
当前程序要么卡在获取新建连接,要么卡在与任意一个叫客户端通信。
解决方法:
1----使用多进程的TCP通信
2---使用多线程的TCP通信
多进程TCP通信
主程序只获取新建连接,获取新建连接之后,为每个新建链接的通信创建一个单独的执行流,负责与指定客户端进行通信。
好处:
1.就算没有新建连接到来,主流程阻塞,也不影响与其他客户端的通信
2. 就算某个客户端没有发送数据,则色指定执行流,不影响其他客户端通信急获取新建链接。
该方案需要注意:
1.父进程创建子进程成功,就要将新建套接字关闭掉释放资源,防止资源泄漏。
2. 自定义信号处理---SIGCHLD,防止僵尸进程产生。
代码实现:只需要将上面的server端的代码修改即可,这样就能与多个客户端进行循环通信。
tcp_server_process.cpp
#include "tcp_socket.hpp"
#include <signal.h>
void worker(TcpSocket &new_sock ){
bool ret;
while(1){
//通过新建连接与客户端通信。
std::string buf;
ret =new_sock.Recv(&buf);
if(ret==false){
new_sock.Close();
return;
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"srv say: ";
std::cin>>buf;
ret=new_sock.Send(buf);
if(ret==false){
new_sock.Close();
return;
}
}
}
int main(int argc,char*argv[])
{
if(argc!=3){
printf("usage: ./tcp_client srvip srvport\n");
return -1;
}
/重定义信号SIGCHLD,避免产生僵尸进程
signal(SIGCHLD,SIG_IGN);//将该信号处理方式定义为忽略;
/
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){
usleep(1000);
continue;
}
std::cout<<"new Connect"<<ip<<":"<<port<<":"<<std::endl;
//创建一个新的子进程与客户端进行通信
pid_t pid =fork();
if(pid==0){
//子进程
worker(new_sock);
new_sock.Close();
exit(0);
}
//父进程
new_sock.Close();//父子进程数据独有,写时拷贝,关闭这个不会影响子进程,不关闭反而存在资源泄漏。
}
lst_sock.Close();
return 0;
}
//makefile
all:tcp_server_process tcp_client
tcp_client:tcp_client.cpp
g++ $^ -o $@ -std=c++11
tcp_server_process:tcp_server_process.cpp
g++ $^ -o $@ -std=c++11
结果演示:
可以看到使用两个客户端通信则父进程会产生两个子进程来完成相对对应的通信,且不阻塞。
多线程TCP通信
多线程与多进程的处理思路比较相似,只需重新修改一下sever端代码即可
tcp_socket_thread.cpp:
#include"tcp_socket.hpp"
#include <pthread.h>
void* worker(void *arg){
TcpSocket* comm_sock=(TcpSocket*)arg;
bool ret;
while(1){
std::string buf;
ret =comm_sock->Recv(&buf);
if(ret==false){
comm_sock->Close();
delete comm_sock;
return NULL;
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"srv say: ";
std::cin>>buf;
ret=comm_sock->Send(buf);
if(ret==false){
comm_sock->Close();
delete comm_sock;
return NULL;
}
}
}
int main(int argc,char*argv[])
{
if(argc!=3){
printf("usage: ./tcp_client srvip srvport\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 *comm_sock=new TcpSocket();
bool ret =lst_sock.Accept(comm_sock);
if(ret==false){
usleep(1000);
continue;
}
std::cout<<"new Connect"<<ip<<":"<<port<<":"<<std::endl;
/
pthread_t tid;
pthread_create(&tid,NULL,worker,(void*)comm_sock);
pthread_detach(tid);
//
}
lst_sock.Close();
return 0;
}
///makefile 注意使用了线程 需要链接线程库
all:tcp_server_thread tcp_client
tcp_client:tcp_client.cpp
g++ $^ -o $@ -std=c++11
tcp_server_thread:tcp_server_thread.cpp
g++ $^ -o $@ -std=c++11 -l pthread
结果和上面多进程效果上差不多。结果不演示了,该方法相比于多进程占用的资源更少,但是多进程的进程间独立性更强。
多线程方案中需要注意的地方:
1.主线程创建普通线程之后,不能随意关闭新建套接字---线程间文件描述符。
2.线程的入口函数传参,需要注意参数的生命周期。