TCP依旧使用代码来熟悉对应的套接字,很多接口都是在udp中使用过的
所以就不会单独把他们拿出来作为标题了,只会把第一次出现的接口作为标题
![](https://img-blog.csdnimg.cn/06397de1c9e7467f99d8b49105da0014.png)
通过TCP的套接字 ,来把数据交付给对方的应用层,完成双方进程的通信
服务端 tcp_server
tcpserver.hpp(封装)
在 tcpServer.hpp 中,创建一个命名空间 yzq 用于封装
在命名空间中,定义一个类 TcpServer
该类中包含 构造 析构 初始化(initServer) 启动(start)
初始化 initServer
1. 创建socket
![](https://img-blog.csdnimg.cn/cc53cbbcd9b24b0d88a38754016b03ea.png)
设置监听端口号(后面会解释) ,需要端口号标识进程的唯一性
![](https://img-blog.csdnimg.cn/8a0f20d37dc04737aac45d5aada08103.png)
在类外设置一个默认端口号8888作为构造函数参数port的缺省值
![](https://img-blog.csdnimg.cn/bf4e33d0bf994d27b1f67dde79e3b035.png)
创建套接字
输入 man socket
![](https://img-blog.csdnimg.cn/692d45240fc24975b162b991a93c1a00.png)
第一个参数 domain ,用于区分 进行网络通信还是 本地通信
若想为网络通信,则使用 AF_INET
若想为本地通信,则使用 AF_UNIX
第二个参数 type, 套接字对应的服务类型
![](/qrcode.jpg)
![](https://img-blog.csdnimg.cn/1006444347564a829236efed39893d50.png)
SOCK_STREAM 流式套接
SOCK_DGRAM 无连接不可靠的通信(用户数据报)
第三个参数 protocol ,表示想用那种协议,协议默认为0
若为 流式套接,则系统会认为是TCP协议 ,若为用户数据报,则系统会认为是UDP协议
套接字的返回值:若成功则返回文件描述符,若失败则返回 -1
![](https://img-blog.csdnimg.cn/906e2ff7d69c43f79b03c263a718da93.png)
说明进行网络通信,流式套接,同时系统认为是TCP协议
![](https://img-blog.csdnimg.cn/14df1983f65448ff8a30930236ab94d4.png)
创建err.hpp 用于存储错误信息的枚举
![](https://img-blog.csdnimg.cn/e45eb9a730834a299d68596f977f8bfe.png)
如果创建失败,则终止程序
2. 绑定 bind
输入 man 2 bind ,查看绑定
![](https://img-blog.csdnimg.cn/44238844cf5b45718c1ba11c69d38026.png)
给一个套接字绑定一个名字
第一个参数 sockfd 为 套接字
第二个参数 addr 为 通用结构体类型
第三个参数 addrlen 为 第二个参数的实际长度大小
bind返回值:若成功,则返回0,若失败,返回 -1
![](https://img-blog.csdnimg.cn/aba948b977b94cbe9426fe313a11d18e.png)
使用bind,是需要借助一个通用结构体来实现的
所以定义一个 网络通信类型的结构体 local
在上一篇博客中,详细讲述了 sockaddr_in 结构体的内部组成
不懂可以去看看:struct sockaddr_in 的理解
htons —— 主机序列转化为网络序列
输入 man htons ,表示短整数的主机转网络序列
![](https://img-blog.csdnimg.cn/5108373a09c442778a9b8c79630d8b1f.png)
所以需要将主机的port_进行转化 ,然后再交给 local的sin_port (端口号)
![](https://img-blog.csdnimg.cn/7b15da33ce784b6782711a4d67733fc5.png)
INADDR_ANY 表示bind的任意IP
![](https://img-blog.csdnimg.cn/5d8cf1ad74fd4553b70ffa13b90382d5.png)
如果绑定失败返回-1
3.监听
listen ——设为 监听状态
输入 man 2 listen
设置当前套接字状态为 监听状态
![](https://img-blog.csdnimg.cn/9ff2025eebeb489e971c477998e30722.png)
第一个参数 sockfd 为 套接字
第二个参数 暂不做解释,一般设为整数
若成功则返回0,若失败返回-1
![](https://img-blog.csdnimg.cn/2df9413d31af4d59ad240239652b1872.png)
监听失败 返回-1,并终止程序
![](https://img-blog.csdnimg.cn/31a2c64caa8a4b5fb54b35b107f5a95e.png)
在类外设置一个 默认整数 为32
启动 Start
![](https://img-blog.csdnimg.cn/db434e964c3d45799318bed490c3ee5c.png)
设置一个布尔变量 quit_,若为true则表示 服务器启动, 若为false,则表示 服务器没有启动
如果服务器没有启动,则进入while循环
![](https://img-blog.csdnimg.cn/0d64255c2a9b46cd806a17f715342242.png)
1.获取连接,accept
accept
输入 man 2 accept
![](https://img-blog.csdnimg.cn/f5125080412a41ec8bd73502bb6e5025.png)
需要知道谁连的你,所以要获取到客户端的相关信息
第一个参数 sockfd 为套接字
第二个参数 addr 为通用结构体类型的 结构体 这个结构体是用来记录客户端内的port号以及IP地址 、16位地址类型等信息
第三个参数 addrlen 为 结构体的大小
返回值:
若成功,则返回一个合法的整数 即文件描述符
若失败,返回-1并且设置错误码
accept返回的文件描述符 与 socket设置成功返回的文件描述符的关系
如:有一个鱼庄,生意不太好,所以在外面站着一个人叫张三,进行揽客
有一天你和你的朋友在外面遇见张三,张三就向你们说他们鱼庄有多少,推荐去他们哪里吃鱼
正好你们俩也饿了,所以就跟张三去鱼庄吃鱼,但是只有你们进入鱼庄了,张三并没有进去
张三只是向里面喊了一声,来客人了,然后继续找人去了
这个时候来了一个服务员李四,向你们询问要吃什么,并向你们提供各种服务
每一次张三把客人招呼到鱼庄时,都会有一名服务员给客人提供服务
当张三做完自己的工作后,立马返回自己的工作岗位,继续招揽客人
张三不给用户提供具体的服务,只负责把客人从路上拉到店里去吃饭 进行消费
李四来给客人提供服务
鱼庄 可以看作是 整个服务器
像张三这样把客人从外部 拉到餐厅里的 称为 监听套接字 即accept的第一个参数 sockfd
像李四这样作的动作,相当于accept会返回一个文件描述符,这个文件描述符 是真正给用户提供IO服务的
若张三继续拉客,在路上碰见一个人,问他要不要去鱼庄吃饭,但那个人摇了摇头,表示没有意愿去鱼庄吃饭,
此时张三就被拒绝了,但这并不影响张三继续拉客去鱼庄
所以 accept 获取失败,只需继续 执行即可
![](https://img-blog.csdnimg.cn/bf3d8609e0114f58927721c1cc445450.png)
2.获取新连接成功,开始进行业务处理
提供一个service的函数 ,参数为新的文件描述符sock
用于实现基本的读写服务 即 客户端发消息,需要把消息转回去
TCP 是一种流式服务
输入 man 2 read
![](https://img-blog.csdnimg.cn/269549ca4a4849cebc65bc8f590a4516.png)
从文件描述符fd中将我们想要的数据,按照数据块的方式读取出来
返回值代表多少字节,读取到文件结尾为0,失败为-1
![](https://img-blog.csdnimg.cn/166b3c38a9974a2891f936af19f3d921.png)
将sock中的数据读取到buffer缓冲区中
若读取成功,则将最后一位的下一位赋值为0
![](https://img-blog.csdnimg.cn/a598f4618048483bb688705929e951c0.png)
若read的返回值为0,则对方将连接关闭了,所以sock也可以关闭
![](https://img-blog.csdnimg.cn/a3b7bace28bc4d459ae76fc0997e7eec.png)
若返回值小于0,则读取失败,返回错误码
收到消息,需要把消息做某种处理后,再把消息转回去
所以使用 包装器 functional处理
![](https://img-blog.csdnimg.cn/843f94b066d04d1181220afb48f1207b.png)
在类外设置一个函数类型,返回值为string,参数为 string 的包装器
![](https://img-blog.csdnimg.cn/9a20e567fd314e69beee6470d5e67d09.png)
用该函数类型定义为一个私有变量func
![](https://img-blog.csdnimg.cn/26d351980ba4451b94ae6eb82d65c5cd.png)
将处理完的消息进行返回
输入 man 2 write
向一个文件中写入信息
![](https://img-blog.csdnimg.cn/3d80c85553414e74a6e6b8028a21a605.png)
fd代表文件描述符
buf代表 缓冲区
count代表 缓冲区大小
write将缓冲区的count大小的数据写入 fd中
![](https://img-blog.csdnimg.cn/c415a52785174a49a52601a6fcf34d0d.png)
将res中的数据 写入 sock文件描述符中
tcpserver.cc (主函数main实现)
想要只输入 ./tcp_server 加 端口号
所以在main函数中添加命令行参数
main函数的两个参数,char* argv[] 为指针数组 ,argv为一张表,包含一个个指针,指针指向字符串
int argc,argc为数组的元素个数
![](https://img-blog.csdnimg.cn/a4034d4dd1034ceca444547b5130ccc0.png)
当参数输入不为2时,就会终止程序,同时打印出对应的输入参数
![](https://img-blog.csdnimg.cn/3b7aa0fcfa094c7eb801f12ab6c40060.png)
通过构造函数了解, 想要使用 new TcpServer 需要传入回调和端口号
![](https://img-blog.csdnimg.cn/4a57acc8a5f94d779f32bf3ee1c721b9.png)
客户端 tcp_client
tcpclient.cc(不封装,直接实现)
为了使用客户端,所以要输入对应的 可执行程序 serverip serverport
所以在main函数需要使用 命令行参数
![](https://img-blog.csdnimg.cn/f49fce76920d4c31ab95d6a56e35c90a.png)
若输入的参数少于3个,则终止程序,并打印出对应输入的参数
![](https://img-blog.csdnimg.cn/8937afb3f12647d893fcde6db5b1c049.png)
将输入的第二个参数的IP地址 赋值给 serverip
将输入的第三个参数的端口号,使用atoi将字符串转化为整数 ,再赋值给serverport
1.创建套接字
![](https://img-blog.csdnimg.cn/4a6e68fd64e948abac681814afb934d4.png)
网络通信,并为流式套接,默认为0,因为流式所以为TCP协议
若创建套接字失败,则终止程序
2.发起链接
输入 man accept
![](https://img-blog.csdnimg.cn/429f1285e4e64d48b7603c03be27f6d3.png)
客户端 通过套接字sockfd,向特定的服务器发起链接请求
sockfd:套接字
addr:公共类型的结构体 内部包含 服务器的IP地址和的端口号
addrlen:结构体的大小
返回值:若成功,则返回0,若失败,返回-1和错误码
首次发起链接时,操作系统会给客户端自动进行绑定端口
所以需要先定义一个结构体server
![](https://img-blog.csdnimg.cn/757f7ccd0db241a197efcfde73911306.png)
借助htons 将上述的主机序列端口号serverport 转化为网络序列端口号
inet_addr——字符串IP地址 转为 网络序列IP地址
输入man inet_addr
![](https://img-blog.csdnimg.cn/01b96eb2df8942ec8d06b7a933ca40a5.png)
第一个参数为 字符串风格的IP地址
第二个参数 为 网络序列的IP地址
将 字符串风格的IP地址 转为 网络序列的IP地址
![](https://img-blog.csdnimg.cn/c5e6833a11034500b8c53d5e3f211588.png)
再将主机序列的IP地址serverip,转化为网络序列的IP地址
![](https://img-blog.csdnimg.cn/11115050c415422493c17245391a93d2.png)
cnt表示重连次数
设置while循环,当不等于0链接失败时,cnt值减1,并重新链接,若cnt值为0,则break终止循环
若出了while循环,cont小于等于0,则终止程序
3. 链接成功
![](https://img-blog.csdnimg.cn/3e5548e399734870bae6862737d879f9.png)
创建一个string类型的line,将输入的参数传入line中
使用write,将line的内容传入文件描述符中
使用read,将sock的数据传入buffer中
通过read的返回值来判断,若返回值大于0则,输出其中内容
若返回值等于0,则说明链接关闭,则退出while循环
若返回值小于,则说明创建失败,返回错误码
具体代码实现
err.hpp(用于存放错误信息)
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,//2
BIND_ERR,//3
LISTEN_ERR//4
};
makefile
.PHONY:all
all: tcp_client tcp_server
tcp_client:tcpClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
tcp_server:tcpServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcp_client tcp_server
tcpServer.hpp( 服务端 封装)
#pragma once
#include<iostream>
#include<cstdlib>
#include<string.h>
#include<unistd.h>
#include"err.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<functional>
namespace yzq
{
static uint16_t defaultport=8888;//默认端口号
static const int backlog=32;//默认整数为32
using func_t=std::function<std::string(const std::string&)>;
class TcpServer;
class ThreadData//该类用于存放客户端的IP port 套接字
{
public:
ThreadData(int fd,const std::string&ip,const uint16_t &port,TcpServer*ts)//构造
:sock(fd),clientip(ip),clientport(port),current(ts)
{
}
public:
int sock;//套接字
std::string clientip;//客户端IP
uint16_t clientport;//客户端端口号
TcpServer*current;
};
class TcpServer
{
public:
TcpServer(func_t func,uint16_t port=defaultport)
:func_(func),port_(port),quit_(true)//表示默认启动
{
}
void initServer()//初始化
{
//1.创建socket
listensock_=socket(AF_INET,SOCK_STREAM,0);
if(listensock_<0)//创建失败
{
std::cout<<" create socket errno"<<std::endl;
exit(SOCKET_ERR);//终止程序
}
//2. bind 绑定
struct sockaddr_in local;//网络通信类型
//清空
memset(&local,'\0',sizeof(local));
local.sin_family=AF_INET;//网络通信
//htons 主机转网络
local.sin_port=htons(port_);//端口号
local.sin_addr.s_addr=INADDR_ANY ; //IP地址
if(bind(listensock_,(struct sockaddr*)&local,sizeof(local))<0)
//失败返回-1
{
std::cout<<" bind socket errno"<<std::endl;
exit(BIND_ERR);//终止程序
}
// 3.监听
if(listen(listensock_,backlog)<0)
{
//监听失败返回-1
std::cout<<" listen socket errno"<<std::endl;
exit(LISTEN_ERR);//终止程序
}
}
void start()//启动
{
quit_=false;//服务器没有启动
while(!quit_)
{
//4.获取连接,accept
struct sockaddr_in client;//网络通信类型
socklen_t len=sizeof(client);//结构体大小
int sock=accept(listensock_,(struct sockaddr*)&client,&len);
if(sock<0)
{
//获取失败
std::cout<<" accept errno"<<std::endl;
continue;//继续执行
}
//提取客户端信息
std::string clientip=inet_ntoa(client.sin_addr);//客户端ip
uint16_t clientport=ntohs(client.sin_port);//客户端端口号
//5.获取新连接成功,开始进行业务处理
std::cout<<"获取新连接成功: "<<sock<<"from "<<listensock_<<std::endl;
//service(sock);//多线程版本没有调用函数
//多线程版本
pthread_t tid;
ThreadData*td=new ThreadData(sock,clientip,clientport,this);
pthread_create(&tid,nullptr,threadRoutine,td);
}
}
static void *threadRoutine(void*args)
{
pthread_detach(pthread_self());//线程分离
ThreadData*td=(ThreadData*)args;
td->current->service(td->sock);
delete td;
return nullptr;
}
void service(int sock)
{
char buffer[1024];
while(true)
{
//将sock中的数据读取到buffer中
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
//读取成功
buffer[s]=0;
//使用func 进行回调
std::string res=func_(buffer);
std::cout<<res<<std::endl;
//将res中的数据写给sock中
write(sock,res.c_str(),res.size());
}
else if(s==0)
{
//说明对方将连接关闭了
close(sock);
std::cout<<"client quit,me too"<<std::endl;
break;
}
else
{
//读取失败返回-1
std::cout<<"read errno"<<strerror(errno)<<std::endl;
break;
}
}
}
~TcpServer()
{
}
private:
func_t func_;//函数类型
int listensock_;//监听套接字
bool quit_;//表示服务器是否启动
uint16_t port_;//端口号
};
}
tcpServer.cc( 服务端 主函数实现)
#include"tcpServer.hpp"
#include<memory>//智能指针
using namespace std;
using namespace yzq;
static void usage(string proc)
{
std::cout<<"usage:\n\t"<<proc<<"port\n"<<std::endl;
}
std::string echo(const std::string&message)
{
return message;
}
// ./tcp_server port
int main(int argc,char*argv[])
{
//输入两个参数 所以不等于2
if(argc!=2)
{
usage(argv[0]);
exit(USAGE_ERR);//终止程序
}
//将输入的端口号 转化为整数
uint16_t port=atoi(argv[1]);
unique_ptr<TcpServer>tsvr(new TcpServer(echo,port));
tsvr->initServer();//服务器初始化
tsvr->start();//启动
return 0;
}
tcpClient.cc(客户端 不封装)
#include<iostream>
#include<cstring>
#include<unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"err.hpp"
using namespace std;
static void usage(string proc)
{
std::cout<<"usage:\n\t"<<proc<<"port\n"<<std::endl;
}
//./tcp_client serverip serverport
int main(int argc,char*argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(USAGE_ERR);//终止程序
}
std::string serverip=argv[1];//IP地址
uint16_t serverport=atoi(argv[2]);//端口号
//1.创建套接字
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
//创建失败
cout<<"socket errnr:"<<strerror(errno)<<endl;
exit(SOCKET_ERR);//终止程序
}
//2.发起链接
struct sockaddr_in server;
memset(&server,0,sizeof(server));//清空
server.sin_family=AF_INET;//网络通信类型
//htons 主机序列转为网络序列
server.sin_port=htons(serverport);//网络端口号
inet_aton(serverip.c_str(),&server.sin_addr);//网络IP地址
int cnt=5;//重连次数
while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)
{
//不等于0则链接失败
sleep(1);
cout<<"正在尝试重连,重连次数还有:"<<cnt--<<endl;
if(cnt<=0)
{
//没有重连次数
break;
}
}
if(cnt<=0)
{
//链接失败
cout<<"链接失败.."<<endl;
exit(SOCKET_ERR);//终止程序
}
char buffer[1024];
//3.链接成功
while(true)
{
string line;
cout<<"enter>>";
getline(cin,line);//从cin中获取内容 写入line中
write(sock,line.c_str(),line.size());//将line中的内容写入到sock文件描述符中
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"server echo"<<buffer<<endl;
}
else if(s==0)
{
cout<<"server quit"<<endl;
break;
}
else
{
cout<<"read errno"<<strerror(errno)<<endl;
break;
}
}
close(sock);
return 0;
}