TCP/IP协议栈
在写代码之前我们先了解什么是协议栈。
根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。TCP套接字是面向连接的,因此又称基于流(stream)的套接字。TCP是 Trasmission Control Protocol (传输控制协议)的简写,意为“对数据传输过程的控制”。
协议栈一般分为4个层次:
第一层次:数据链路层
链路层是物理链接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN 等网络标准。
第二层次:IP层
IP是面向消息的、不可靠的协议。每次传输数据时会帮我们选择路径,但并不一致。如果传输中发生路径错误,则选择其他路径;但如果发生数据丢失或错误,则无法解决。换言之,IP协议是无法应对数据错误的。
第三层次:TCP/UDP 层
IP层解决数据传输中的路径选择问题,只需照此路径传输数据即可。
TCP和UDP 层以 IP层提供的路径信息为基础完成实际的数据传输,故该层又称传输层(Transport)。
第四层次:应用层
前面三个层次,套接字通信过程中都是自动处理的。为了”使程序员从这些细节中解放出来“。选择数据传输路径、数据确认过程都被隐藏到套接字内部。前面三个层次都是为了给应用层提供服务的。
简单了解了一下TCP的底层原理我们就开始写一个简单的迭代服务器来进一步理解TCP
首先我们先来编写服务端代码
1. 第一步准备好需要使用的头文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
2. 第二步开始创建套接字
void sokc_ls()
{
//创建套接字(socket)
int serv_sock;
//这是一个基于IPv4的TCP套接字
serv_sock = socket(PF_INET,SOCK_STREAM,0);
if(serv_sock < 0)//判断套接字(socket)是否创建成功
{
//在一般的编程中防御性编程是十分重要的
std::cout<<"creat sock failed!"<<std::endl;
return;
}
}
3. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));//将servaddr清0 预防申请的变量内有残留信息存在造成错误
//配置地址
servaddr.sin_family = AF_INET;//IPv4协议族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//相当于”0.0.0.0“ 监听所有地址
servaddr.sin_port = htons(9527);//分配端口
//配置完地址 就可以开始分配了
int ret = bind(serv_sock,(struct sockaddr*)&servaddr,sizeof(servaddr));
if(ret == -1)//成功时返回0 失败时返回-1
{
std::cout<<"bind failed!"<<std::endl;
close(server);
return;
}
4. 等待连接请求(监听)
int ret = listen(serv_sock,3);//第二个参数表示可监听的数量
if(ret == -1)//成功返回0 失败返回-1
{
std::cout<<"listen failed!"<<std::endl;
close(server);
return;
}
5. 创建分机 ,并与客户端连接
int client;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
char buffer[1024];
while(1)
{
memset(buffer,0,sizeof(buffer));//将buffer清0 防止有残留数据存在
client = accept(serv_sock,(struct sockaddr*)&cliaddr,&cliaddrlen);
if(client == -1)//成功时返回创建的套接字文件描述符,失败时返回-1
{
std::cout << "accept failed !" << std::endl;
close(server);
return;
}
//读取数据
read(client,buffer,sizeof(buffer));
//写入数据
ssize_t len = write(client,buffer,stelen(buffer));
if(len != (ssize_t)strlen(buffer))//返回的长度与发送的长度不一致
{
std::cout<<"write failed!"<<std::endl;
close(serv_sock);
return;
}
//关闭套接字
close(client);//这步可不执行
}
close(serv_sock);//当服务端关闭时,客户端也会同时关闭。
服务端代码写完,我们再来写客户端代码
void client_ls()
{
//创建套接字
int client;
struct sockaddr_in servaddr;
client = socket(PF_INET,SOCK_STREAM.0);
if(serv_sock < 0)//判断套接字(socket)是否创建成功
{
std::cout<<"creat sock failed!"<<std::endl;
return;
}
memset(servaddr,0,sizeof(servaddr));//清0
//配置地址
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//”127.0.0.1“为回环地址 一般在本机调试时使用
servaddr.sin_port = htons(9527);
//与服务端连接
int ret = connect(client,(struct sockaddr*)&servaddr,sizeof(servaddr));
if(ret==0)//成功时返回0 失败时返回-1
{
char buffer[256] = "hello,here is client!";
//往服务端写入数据
write(client,buffer,strlen(buffer));
memset(buffer,0,sizeof(buffer));//清0
//读取从服务端返回的数据
read(client,buffer,sizeof(buffer));
std::cout<<buffer;//输出读取的数据
}
else
{
printf("%s(%d):%s %d\n", __FILE__, __LINE__, __FUNCTION__, ret);
}
//关闭套接字
close(client);
std::cout<<"client done !"<<std::endl;
}
服务端与客户端的代码都写完了,我们来测试一下
int main(int argc,char* argv[])
{
//使用创建子进程的方法来进行测试
pid_t pid = fork();
if(pid == 0)//当pid等于0 时,代表的是子进程 所以我们调用客户端函数
{
sleep(1);//暂停一秒 防止第一次连接失败
client_ls();
client_ls();
}
else if(pid > 0)//当pid大于0时,代表的是父进程,所以我们调用服务端函数
{
sokc_ls();
int status = 0;
writ(&status);//为防止僵尸进程的出现 使用fork函数时,推荐将writ函数与fork函数同时写入,在进行内容编辑
}
else
{
std::cout << "fork failed!" << pid << std::endl;//进程创建失败
}
}
因为没有设置退出机制所以会在俩次客户端执行结束后卡住,可使用Ctrl+C进行退出。