TCP基本用法
- TCP设计模式
- C/S(client-server):客户端-服务器模式
- UDP则是peer-to-peer模式,即:对等模式,两端地位相等,而TCP两端地位不等
- TCP Socket的工作模式
- 客户端
(1)连接服务器
(2)通讯 - 服务器
(1)主Socket接受连接
(2)当有连接到来时,创建一个WorkingSocket为Client Socket提供服务 - 注:每个Client都分配有一个Socket,专门地、一对一地提供服务
- 示例
- TCP中应该使用多线程技术
- 服务器
(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)创建: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)请求/应答模式
一般情况下,需要客户端发起一个请求(request),然后服务器做出针对性的应答。即:客户端是主动的,服务器是被动的
(2)服务器应该处于常开的状态,服务器程序应该保持一直运行,随时等待由Client发起请求
(3)Send/Recv不需要再指定目标地址,在Connect成功之后,Client和服务器的某个Working Socket已经配对成功,变为一对一的通话
(4)服务器一般只需要指定端口号OS_SocketAddr(9002);相当于OS_Socket("127.0.0.1",9002);
TCP内部缓冲区
- 发送/接收缓冲区——与UDP相比
- 相同点
每个Socket都拥有一个发送缓冲区和接受缓冲区 - 不同点
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;
}
- 定义边界
-
背景:由于TCP Socket是流式存取,如何判断Recv()已经取走了全部数据(接收方是不知道发送方发送数据的大小)
-
方法
(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时,就知道接受完毕了
- 阻塞
- Send()阻塞:当发送缓冲区满的时候
例如:发送方不停发送数据,将接收缓冲区填满,此时发送方操作系统就无法发送数据,最终导致发送缓冲区满,Send()阻塞 - Recv()阻塞:当接收缓冲区为空的时候
- 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)交换机记录了每个口的主机的IP,选择对应的口转发 - 发送:操作系统把数据包通过网卡、上行传输到交换机
- 接收:操作系统从网卡获取数据包
- UDP的传输
- 正常的传输流程:
(1)主机A发出一个UDP包并抵达交换机的1口
(2)交换机将此包复制到2,3,4口
(3)处于2口的主机B,接收到这个包 - 不正常的情况:交换机丢掉了这个包,没有转到2口
- 问题:主机A无法得知这个包有没有抵达目标主机
- UDP是不可靠的传输协议
(1)A将数据包发出后,可能会丢包,无法抵达B
(2)当(1)发生时,A无法得知此包已经被丢失。注:A接收B的应答属于B发送一个数据包给A,属于我们的设计
(3)UDP的发送端不会失败,它只负责将数据发送出去,不会负责数据是否抵达
- TCP的传输
- 正常的传输流程
(1)主机A发送数据包,抵达1口
(2)交换机将1口的数据复制到2口
(3)主机B接收到包后,会回发一个确认包(操作系统完成)
(4)交换机将2口的确认包复制到1口
(5)主机A收到确认包 - TCP是一来一回,带有确认回复的,当没有收到确认包时,操作系统会重发数据包
- TCP是可靠的传输协议
(1)发送端能够知道数据包有没有抵达目标,并且操作系统还有重发机制
(2)TCP的发送端可能会失败,当网络断开或者没有收到确认包时,就是发送失败了
Select查询机制
- Select
- select是一个函数,用于向操作系统查询,即:在一堆socket中,查出可以读、写的socket
- 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;
}
- select函数简介
//fds表示socket列表
//tm表示超时
//具体信息可以查看OSAPI中的Socket.cpp
select(hSock+1,&fds,NULL,NULL,&tm);
//在指定时间内,有任意的socket处于可读的状态,则select立刻返回,在fds中只剩下可读的socket。返回值为socket个数
//到了指定时间,没有任何socket可读,则返回0,意味超时
//如果出错返回-1
- select用途
当服务器同时和大量客户端交互,可以用select查询那些socket有发送来的数据,才做响应