c++ TCP相关——基于OSAPI

TCP基本用法

  • TCP设计模式
  1. C/S(client-server):客户端-服务器模式
  2. UDP则是peer-to-peer模式,即:对等模式,两端地位相等,而TCP两端地位不等
  • TCP Socket的工作模式
  1. 客户端
    (1)连接服务器
    (2)通讯
  2. 服务器
    (1)主Socket接受连接
    (2)当有连接到来时,创建一个WorkingSocket为Client Socket提供服务
  3. 注:每个Client都分配有一个Socket,专门地、一对一地提供服务
  • 示例
  1. TCP中应该使用多线程技术
  2. 服务器
    (1)创建:serverSock.Open(OS_SockAddr(9000),true);
    (2)监听:serverSock.Listen();
    (3)接收:serverSock.Accept(&workSock);
//TcpWork.h
#pragma once
#include"osapi/osapi.h"
class TcpWork :
    public OS_Thread
{
private:
    OS_TcpSocket workSocket;
public:
    TcpWork(OS_TcpSocket workSocket):workSocket(workSocket){}
private:
    int Routine();
};
//TcpWork.cpp
#include "TcpWork.h"
int TcpWork::Routine() {
	//为client提供服务
	char buf[128];
	//接受客户的请求
	int n = workSocket.Recv(buf, 128);
	buf[n] = 0;
	printf_s("客户请求:%s\n", buf);

	//应答客户
	strcpy_s(buf, "我已收到\n");
	n = strlen(buf);
	workSocket.Send(buf, n);
	workSocket.Close();
	return 0;
}
#include<iostream>
#include"osapi/osapi.h"
#include"TcpWork.h"

using namespace std;

int main() {
	//创建server socket
	OS_SockAddr local("127.0.0.1", 9002);
	OS_TcpSocket serverSocket;
	serverSocket.Open(local, true);

	//监听请求,没有请求会阻塞
	serverSocket.Listen();

	while (true) {
		//接受请求,并创建workSocket
		OS_TcpSocket workSocket;
		if (serverSocket.Accept(&workSocket) < 0) {
			break;
		}

		//新建一个线程,处理client的请求
        //?如何销毁
		TcpWork* conn = new TcpWork(workSocket);//不能TcpWork tcpWork(workSocket)
		conn->Run();
	}
	return 0;
}
  1. 客户端
    (1)创建:Open();
    (2)连接:Connect();
    (3)发送:Send();
    (4)接收:Recv();
#include<iostream>
#include"osapi/osapi.h"

using namespace std;

/**
	客户端
**/
int main() {
	//打开Socket
	OS_SockAddr local("127.0.0.1", 9005);
	OS_TcpSocket clientSock;
	clientSock.Open(local,true);

	//连接服务器
	OS_SockAddr serverAddr("127.0.0.1", 9002);
	if (clientSock.Connect(serverAddr) < 0) {
		cout << "无法连接服务器" << endl;
		return -1;
	}

	char buf[128];

	//发送请求
	strcpy_s(buf, "I'm client\n");
	int n = strlen(buf);
	clientSock.Send(buf, n);

	//接受应答
	n = clientSock.Recv(buf, sizeof(buf));
	buf[n] = 0;
	printf_s("Got:%s\n", buf);

	//关闭Socket
	clientSock.Close();
	return 0;
}
  1. 注意事项
    (1)请求/应答模式
    一般情况下,需要客户端发起一个请求(request),然后服务器做出针对性的应答。即:客户端主动的,服务器被动
    (2)服务器应该处于常开的状态,服务器程序应该保持一直运行,随时等待由Client发起请求
    (3)Send/Recv不需要再指定目标地址,在Connect成功之后,Client和服务器的某个Working Socket已经配对成功,变为一对一的通话
    (4)服务器一般只需要指定端口号OS_SocketAddr(9002);相当于OS_Socket("127.0.0.1",9002);

TCP内部缓冲区

  • 发送/接收缓冲区——与UDP相比
  1. 相同点
    每个Socket都拥有一个发送缓冲区和接受缓冲区
  2. 不同点
    UDP Socket的缓冲区:包式存取,每个包带地址
    TCP Socket的缓冲区:流式存取,每个包不带地址
  • 流式存储——针对缓冲区
    类似于管道中的水,第一次放出200斤的水到盆中,第二次放出300斤的水到盆中,然后你在盆中取的时候,是无法区分哪些来自第一次,哪些来自第二次,它们是没有界限的。
    例如:发送数据时,第一次发送hello,第二次发送word,两次达到接受缓冲区后,操作系统取出的就是helloword,没法区分界限了
//服务器端和上述一致
#include<iostream>
#include"osapi/osapi.h"

using namespace std;

/**
	客户端
**/
int main() {
	//打开Socket
	OS_SockAddr local("127.0.0.1", 9005);
	OS_TcpSocket clientSock;
	clientSock.Open(local,true);

	//连接服务器
	OS_SockAddr serverAddr("127.0.0.1", 9002);
	if (clientSock.Connect(serverAddr) < 0) {
		cout << "无法连接服务器" << endl;
		return -1;
	}

	char buf[128];

	clientSock.Send("hello", 5);
	clientSock.Send("world", 5);
	
	//接受应答
	int n = clientSock.Recv(buf, 128);
	buf[n] = 0;
	printf_s("Got:%s\n", buf);

	//关闭Socket
	clientSock.Close();
	return 0;
}
  • 定义边界
  1. 背景:由于TCP Socket是流式存取,如何判断Recv()已经取走了全部数据(接收方是不知道发送方发送数据的大小)

  2. 方法

    (1)先发送长度,后发送数据。例如:05 hello,发送方先发送长度,接受方接受后,发送方再发送数据

int WaitBytes(OS_TcpSocket sock, void* buf, int count, int timeout) {
	//设置超时
	if (timeout > 0) {
		sock.SetOpt_RecvTimeout(timeout);
	}

	//反复接受,知道接满指定的字节数
	int bytes = 0;
	while (bytes < count) {
		int n = sock.Recv((char*)buf + bytes, count - bytes);
		if (n <= 0) {
			return bytes;
		}
		bytes += n;
	}
	return bytes;
}
//客户端
#include<iostream>
#include"osapi/osapi.h"

using namespace std;

//unsigned short类型转换为按大端方式的2个字节
inline void itob_16be(unsigned short a, unsigned char bytes[])
{
	bytes[0] = (unsigned char)(a >> 8);
	bytes[1] = (unsigned char)(a);
}

/**
	客户端
**/
int main() {
	//打开Socket
	OS_SockAddr local("127.0.0.1", 9005);
	OS_TcpSocket clientSock;
	clientSock.Open(local,true);

	//连接服务器
	OS_SockAddr serverAddr("127.0.0.1", 9002);
	if (clientSock.Connect(serverAddr) < 0) {
		cout << "无法连接服务器" << endl;
		return -1;
	}

	char buf[128];

	unsigned char bytes[2];
	itob_16be(10, bytes);//长度为5个字节,并转换为2个字节放在bytes中
	clientSock.Send(bytes, 2);
	clientSock.Send("helloworld", 10);
	
	//接受应答
	int n = clientSock.Recv(buf, 128);
	buf[n] = 0;
	printf_s("Got:%s\n", buf);

	//关闭Socket
	clientSock.Close();
	return 0;
}
//服务端,改动TcpWork.cpp
#include "TcpWork.h"

//将按大端方式的2个字节转换为unsigned short类型
inline unsigned short btoi_16be(unsigned char bytes[])
{
	unsigned short a = 0;
	a += (bytes[0] << 8);
	a += (bytes[1]);
	return a;
}

int TcpWork::WaitBytes(OS_TcpSocket sock, void* buf, int count, int timeout) {
	//设置超时
	if (timeout > 0) {
		sock.SetOpt_RecvTimeout(timeout);
	}

	//反复接受,知道接满指定的字节数
	int bytes = 0;
	while (bytes < count) {
		int n = sock.Recv((char*)buf + bytes, count - bytes);
		if (n <= 0) {
			return bytes;
		}
		bytes += n;
	}
	return bytes;
}

int TcpWork::Routine() {
	//为client提供服务
	char buf[128];
	unsigned char length[2];

	//使用边界,获取接受数据的长度
	WaitBytes(workSocket, length, 2);
	unsigned short count = btoi_16be(length);
	int n = WaitBytes(workSocket, buf, count);
	buf[n] = 0;
	printf("Got:%s\n", buf);
	

	//应答客户
	workSocket.Send("我接受到了\n", 12);

	workSocket.Close();
	return 0;
}

(2)每段消息加上结束符(结束符不属于正文)

例如:hello\n,当接受方接受到\n时,就知道接受完毕了

  • 阻塞
  1. Send()阻塞:当发送缓冲区满的时候
    例如:发送方不停发送数据,将接收缓冲区填满,此时发送方操作系统就无法发送数据,最终导致发送缓冲区满,Send()阻塞
  2. Recv()阻塞:当接收缓冲区为空的时候
  3. Socket默认是阻塞方式,也可以手工设置为非阻塞方式
//设置成非阻塞模式
sock.Ioctl_SetBlockedIo(false);
  • 获取缓冲区大小
// 获取发送缓冲区的大小 
int bufsize = 0; 
socklen_t len = 4;
int ret = getsockopt(clientSocket.hSock,SOL_SOCKET,
			SO_SNDBUF,
			(char*)&bufsize,&len);
if(ret < 0){
	// 获取失败
}
  • 设置缓冲区大小
// 设置发送缓冲区的大小
int bufsize = 128*1024; // 128K
int ret = setsockopt(clientSocket.hSock,SOL_SOCKET,
			SO_SNDBUF,
			(const char*)&bufsize,sizeof(int));
if(ret < 0){
	// 设置失败
}

数据包的传输

  • 数据的发送/接收
  1. 交换机的职责:交换数据,即:将从一个口进入数据的数据包转发到其他口
  2. 交换机的转发
    (1)交换机直接将数据包复制转发到每一个口
    (2)交换机记录了每个口的主机的IP,选择对应的口转发
  3. 发送:操作系统把数据包通过网卡、上行传输到交换机
  4. 接收:操作系统从网卡获取数据包
  • UDP的传输
  1. 正常的传输流程:
    (1)主机A发出一个UDP包并抵达交换机的1口
    (2)交换机将此包复制到2,3,4口
    (3)处于2口的主机B,接收到这个包
  2. 不正常的情况:交换机丢掉了这个包,没有转到2口
  3. 问题:主机A无法得知这个包有没有抵达目标主机
  4. UDP是不可靠的传输协议
    (1)A将数据包发出后,可能会丢包,无法抵达B
    (2)当(1)发生时,A无法得知此包已经被丢失。注:A接收B的应答属于B发送一个数据包给A,属于我们的设计
    (3)UDP的发送端不会失败,它只负责将数据发送出去,不会负责数据是否抵达
  • TCP的传输
  1. 正常的传输流程
    (1)主机A发送数据包,抵达1口
    (2)交换机将1口的数据复制到2口
    (3)主机B接收到包后,会回发一个确认包操作系统完成
    (4)交换机将2口的确认包复制到1口
    (5)主机A收到确认包
  2. TCP是一来一回,带有确认回复的,当没有收到确认包时,操作系统会重发数据包
  3. TCP是可靠的传输协议
    (1)发送端能够知道数据包有没有抵达目标,并且操作系统还有重发机制
    (2)TCP的发送端可能会失败,当网络断开或者没有收到确认包时,就是发送失败了

Select查询机制

  • Select
  1. select是一个函数,用于向操作系统查询,即:在一堆socket中,查出可以读、写的socket
  2. OS_Socket对其做了一个封装,用于查询单个socket是否可以读写
//返回值:>0表示可以读或写
//<0表示不可读或写
//=0表示超时
//timeout单位为毫秒
int Select_ForReading(int timeout);
int Select_ForWriting(int timeout);
//客户端
#include<iostream>
#include"osapi/osapi.h"

using namespace std;


/**
	客户端
**/
int main() {
	//打开Socket
	OS_SockAddr local("127.0.0.1", 9005);
	OS_TcpSocket clientSock;
	clientSock.Open(local,true);

	//连接服务器
	OS_SockAddr serverAddr("127.0.0.1", 9002);
	if (clientSock.Connect(serverAddr) < 0) {
		cout << "无法连接服务器" << endl;
		return -1;
	}

	char buf[128];
	int n;

	//发送请求
	strcpy_s(buf, "I'm client\n");
	n = strlen(buf);
	clientSock.Send(buf, n);

	//select
	cout << "wait……" << endl;
    //等待6秒的原因是服务器需要等待5秒才响应
    //小于5秒会超时
	int ret = clientSock.Select_ForReading(6000);
	cout << "ret:" << ret << endl;

	//接受应答
	n = clientSock.Recv(buf, 128);
	buf[n] = 0;
	printf_s("Got:%s\n", buf);

	//关闭Socket
	clientSock.Close();
	return 0;
}
//服务器端的TcpWork.cpp
#include "TcpWork.h"

int TcpWork::Routine() {
	//为client提供服务
	char buf[128];
	
	//接收客户的请求
	int n = workSocket.Recv(buf, 128);
	buf[n] = 0;
	printf_s("客户请求:%s\n", buf);

	OS_Thread::Msleep(5000);

	//应答客户
	workSocket.Send("我接受到了\n", 12);

	workSocket.Close();
	return 0;
}
  1. select函数简介
//fds表示socket列表
//tm表示超时
//具体信息可以查看OSAPI中的Socket.cpp
select(hSock+1,&fds,NULL,NULL,&tm);
//在指定时间内,有任意的socket处于可读的状态,则select立刻返回,在fds中只剩下可读的socket。返回值为socket个数
//到了指定时间,没有任何socket可读,则返回0,意味超时
//如果出错返回-1
  1. select用途
    当服务器同时和大量客户端交互,可以用select查询那些socket有发送来的数据,才做响应