TCP/IP基础知识复习

/*
2018-11-14 09:06:39
基本的准备工作,需要注意的点
在windows环境下
*/
头文件:winsock2.h
链接库:ws2_32.lib
预处理器 加上下面这个
_WINSOCK_DEPRECATED_NO_WARNINGS


server: 基本结构
WSAStartup    由进程启动使用Winsock DLL

Step1     socket    创建套接字
Step2     bind    绑定端口和地址
Step3     listen    监听客户调
Step4    accept    等待客户请求到来 并且返回客户端的相关信息 成功时 返回套接字句柄
    根据得到的socket和地址信息 可以进行给客户端传递信息
    进行send 或者 recv 操作
    
WSACleanup()    对Winsock dll 资源的释放

*************************
Client操作

WSAStartup    由进程启动使用Winsock DLL

Step1    socket    创建套接字
Step2     connect    向服务器发出连接请求
    然后就可以使用send 和 recv进行相应的 操作

WSACleanup()    对Winsock dll 资源的释放

//现在最高的版本是2.2 向下兼容


/*
2018-11-14 10:02:47
ch02 套接字类型和协议设置
*/
协议:计算机间对话必备通信规则
说通俗点:协议就是为了完成数据交换而定好的约定

函数详细解释
例子:SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
函数原型:
    SOCKET socket(
          int af,        //套接字中使用的协议族 信息
          int type,        //套接字数据传输类型信息
          int protocol    //计算机间通信中使用的协议信息
        );
        
参数1详细解释,协议族(可取的值)
PF_INET            ipv4互联网协议族
PF_INET6        ipv6互联网协议族
PF_LOCAL        本地通信的UNIX协议族
PF_PACKET        底层套接字协议族
PF_IPX            IPX Novell协议族

参数2详细解释 套接字类型
套接字类型1 =>    SOCK_STREAM 面向连接的套接字
    特征:
        a:传输过程中数据不会丢失
        b:按序传输数据
        c:传输的数据不存在数据边界
    概括:
        套接字连接必须 一一对应
        可靠的 按序传递 基于字节的面向连接数据的传输方式的套接字

套接字类型2 =>    SOCK_DGRAM 创建面向消息的套接字
    支持datagrams, datagrams是无连接的、不可靠的最大长度(通常很小)的缓冲区。
    使用UDP的互联网地址族
    

参数3详细解释 
    选择1:IPPROTO_TCP    满足条件参数1为PF_INET; 参数2为 SOCK_STREAM
    选择2:IPPROTO_UDP    满足条件参数1为PF_INET; 参数2为 SOCK_DGRAM
    

参照第一章的demo


/*
2018-11-14 10:51:19
ch03 地址族与数据序列
*/
ip:网络协议的缩写 为收发网络数据而分配给计算机的值
端口号:区分程序中创建的套接字而分配的序号

端口号由16位构成:0 - 65535
0 - 1023 一般分配给特定的程序使用
TCP和UDP套接字不会共用端口号 所以允许重复

bind函数原型
int bind( 
    SOCKET s,                          
      const struct sockaddr FAR *name,
    int namelen 
);

主要介绍参数2:

struct sockaddr
{
  u_short    sa_family;
  char       sa_data[14];
};

在绑定参数的时 一般使用下面的结构 来进行参数的分配
上下两个结构体 大小相同


struct sockaddr_in 
{  
    short            sin_family;  // 2 bytes e.g. AF_INET, AF_INET6  
    unsigned short   sin_port;    // 2 bytes e.g. htons(3490)  
    struct in_addr   sin_addr;    // 4 bytes see struct in_addr, below  
    char             sin_zero[8]; // 8 bytes zero this if you want to  
};  
  
typedef struct in_addr {
        union {
                struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { USHORT s_w1,s_w2; } S_un_w;
                ULONG S_addr;
        } S_un;
#define s_addr  S_un.S_addr         /* can be used for most tcp & ip code */
#define s_host  S_un.S_un_b.s_b2    // host on imp
#define s_net   S_un.S_un_b.s_b1    // network
#define s_imp   S_un.S_un_w.s_w2    // imp
#define s_impno S_un.S_un_b.s_b4    // imp #
#define s_lh    S_un.S_un_b.s_b3    // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;


字节序和网络字节序
CPU保存数据的方式有两种
大端序:高位字节存放到低位地址
小端序:高位字节存放到高位地址

有数:0x12345678
大端显示:12 34 56 78
小端显示:78 56 34 12

通过网络数据传输时 约定统一的传输方式 统一大端

给结构体 SOCKADDR_IN 赋值的时候 需要相应的字节序转换
代码片段:
SOCKADDR_IN addrSrv;
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(8888); //端口
    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.168.0.1");

函数 htons,htonl 详细介绍
h表示主机字节库
n表示网络字节库
s表示short类型
l表示long类型

htons:表示将short类型数据从主机字节序 转换为 网络字节序

除了向sockaddr_in结构体中填充数据外,其他情况无需考虑字节序问题

inet_addr函数 将字符串形式的IP地址 转换成32位的网络字节序
相反的函数
inet_ntoa函数 将32位的网络字节 转换为以字符串显示的ip


/*
2018-11-14 10:51:19
ch04 基于TCP的服务器/客户端(一)
*/
TCP/IP 协议栈 分成4层

由下往上:链路层 -> IP层 -> TCP/UDP    -> 应用层

1.链路层
    物理链接,LAN、WAN、MAN 等网络标准
    
2.IP层
    IP本身是面向消息的、不可靠的协议;IP协议无法应对数据错误‘
    解决传输中路径选择的问题
    
3.TCP/UDP层
    TCP :保证数据传输的可靠性 以IP层为基础

4.应用层
    根据程序特点决定服务器端和客户端之间的数据传输协议


**进入连接请求 等待状态
关键函数listen
函数原型:
int listen(
      SOCKET s,        //服务端套接字
      int backlog      //最大的连接请求个数
);

接收客户端的连接请求
SOCKET accept(
      SOCKET s,    //服务端的套接字
      struct sockaddr FAR *addr,    //SOCKADDR_IN 数据结构
    int FAR *addrlen    //数据结构的长度
);

返回客户端的套接字 和 相应的SOCKADDR_IN 的结构信息

接下来就可以在服务端上进行收发消息


**客户端的结构
客户端 最关键的点就是请求连接
函数原型:
int connect(
    SOCKET s,    //客户端套接字的描述符
    const struct sockaddr FAR *name,      //SOCKADDR_IN 数据结构 保存目标服务器端的地址信息和变量地址信息
    int namelen   //      数据结构的长度         
);

客户端调用connect函数 发生以下情况才能返回
1.服务器端接收连接请求
2.发生断网异常


补充:关于recv函数
函数原型:
int recv(  
    SOCKET s,    //指定发送端套接字描述符
    char FAR *buf,    //数据
    int len,        //数据长度
    int flags        //指定调用的方式标志 一般为0
);
如果成功 则返回接收到的字节数

send函数
函数原型:(数据类型同上)
int send(
      SOCKET s,              
      const char FAR *buf,  
     int len,               
      int flags              
);


/*
2018-11-14 16:46:53
ch05 基于TCP的服务器/客户端(二)
*/
灵活的使用send和recv返回的字节长度 可以有效的使用数据
协议:制定一些规则来做一些事情

IO缓冲区概念:
1.IO缓冲在每个TCP套接字中单独存在
2.IO缓冲在创建套接字的时候自动生成
3.即使关闭套接字也会继续传递输出缓冲中遗留的数据
4.关闭套接字将丢失缓冲区的数据


TCP 内部工作原理1: 与对方套接字的连接
TCP套接字创建到消失所经过程分为如下3步
Step1 与对方套接字建立连接
Step2 与对方套接字进行数据交换
Step3 断开与对方套接字的连接

TCP 内部工作原理2: 与对方主机的数据交换
通过第一步三次握手过程完成了数据交换准备
按照以下公式传递 ACK消息
ACK号 -> SEQ号 +  传递的字节数 + 1

TCP 内部工作原理3: 断开与套接字的连接
先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递断开的消息
套接字A同样发出确认消息
各发一次消息 然后断开连接

/*
2018-11-14 19:17:55
ch06 基于UDP的服务器/客户端
*/
UDP套接字的一个特点:性能比TCP高出很多(这里指的是效率)
在更注重性能而非可靠性的情况下 UDP 是一种很好的选择

UDP的内部工作原理
UDP最重要的作用就是根据端口号 将 传到主机的数据包交付给最终的UDP套接字

TCP比UDP慢的原因
1.收发数据前后进行的连接设置及消除过程
2.收发数据过程中为保证可靠性而添加的流控制

UDP的交互方式
=> UDP中只有创建套接字的过程和数据交换的过程
=> UDP服务器端 和 客户端 均只需1个套接字

关于UDP通讯的相关函数
sendto函数 发送消息的函数
函数原型:
int sendto(
      SOCKET s,                    //用于传输数据的UDP套接字文件描述符          
      const char FAR *buf,        //保存待传输数据的缓冲地址值
      int len,                    //传输数据的长度 以字节为单位
      int flags,                  //可选项参数 若没有则 传递为0
      const struct sockaddr FAR *to,  //存有目标地址信息sockaddr结构体变量的地址值
      int tolen                        //传递给参数to的长度    
);

成功返回发送字节的长度 失败返回-1

UDP客户端套接字的地址分配
=>调用sendto函数时自动分配IP和端口号,因此,UDP客户端中通常无需额外的地址分配过程。

UDP的数据传输特性和调用connect函数
=> UDP是具有数据边界的协议,传输中调用I/O函数的次数非常重要。因此输入函数的调用次数
和输出函数的调用次数 应该完全一致,这样才能保证接收全部已发送的数据。


通过sendto传输数据大致分为以下3个阶段
Step1 向UDP套接注册目标IP和端口号
Step2 传输数据
Step3 删除UDP套接字中注册的目标地址信息


分析基于windows实现的 过程 (linux暂时放一下)
windows下的sendto函数和readfrom函数

int sendto(  
    SOCKET s,
    const char FAR *buf,            
      int len,
      int flags,                       
      const struct sockaddr FAR *to,
      int tolen
);

上面已经有函数的各个介绍了 现在我们来撸例子

TCP/IP例子

//Hello_Server 服务端
#include <iostream>
#include <winsock2.h>
#include <string>
#include <vector>

using namespace std;

void ErrorHandling(const char *message);

int Comunicator();
void TestOrder();

int main()
{
	Comunicator();
	//TestOrder();
	system("pause");
	return EXIT_SUCCESS;
}

void ErrorHandling(const char * message)
{
	std::cout << message << std::endl;
}

int Comunicator()
{
	std::vector<std::string> vctCollect;
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() Error");
		return EXIT_FAILURE;
	}

	//Step1 创建套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888); //端口
	addrSrv.sin_addr.s_addr = htonl(INADDR_ANY);	//地址

	//Step2 绑定端口和地址
	int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
	if (retVal == SOCKET_ERROR)
	{
		ErrorHandling("bind() Error");
		return EXIT_FAILURE;
	}

	//Step3 监听客户调
	if (listen(sockSrv, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() Error");
		return EXIT_FAILURE;
	}

	char message[] = "收到请求";

	SOCKADDR_IN addrClient;

	//Step4 等待客户请求到来	
	int length = sizeof(SOCKADDR);
	SOCKET sockConn = accept(sockSrv, (SOCKADDR *)&addrClient, &length);
	if (sockConn == SOCKET_ERROR)
	{
		ErrorHandling("accept() Error");
		return EXIT_FAILURE;
	}

	std::cout << "接收客户端的IP为:" << inet_ntoa(addrClient.sin_addr);
	std::cout << std::endl;

	//接收客户端发回的消息
	while (1)
	{
		char recvBuf[100] = {};
		std::string strBuf = "";
		memset(recvBuf, 0, sizeof(recvBuf));
		int length = recv(sockConn, recvBuf, sizeof(recvBuf), 0);
		for (int i = 0; i < length; ++i)
		{
			strBuf += recvBuf[i];
		}
		//std::cout << "接收客户端的消息:" << strBuf << std::endl;
		vctCollect.push_back(strBuf);
		
		//send(sockConn, message, sizeof(message), 0);
		//Step5 发送数据给客户端
		//std::string message = "";
		//std::cout << "发送给客户端的消息:";
		//std::cin >> message;

		////send函数 成功时 返回发送的字符长度 失败的时候 返回SOCKET_ERROR
		//int iSend = send(sockConn, message.c_str(), message.length(), 0);
		//if (iSend == SOCKET_ERROR)
		//{
		//	ErrorHandling("send() Error");
		//	return EXIT_FAILURE;
		//}

		char ch = vctCollect[vctCollect.size() - 1][0];
		int iResult = 0;
		int num = 0;
		switch (ch)
		{
		case '+':
			num = stoi(vctCollect[0]);
			if ((num + 2) == vctCollect.size())
			{
				for (size_t i = 1; i < vctCollect.size() - 1; ++i)
				{
					iResult += stoi(vctCollect[1]);
				}
			}
			break;
		default:
			break;
		}
		
		if (iResult != 0)
		{
			string strResult = to_string(iResult);
			send(sockConn, strResult.c_str(), strResult.length(), 0);
			break;
		}

		if (strBuf == "exit" || "exit" == message)
		{
			break;
		}

		std::cout << std::endl;
		
	}

	closesocket(sockConn);
	closesocket(sockSrv);
	WSACleanup();

	return EXIT_SUCCESS;
}

//测试大小端
void TestOrder()
{
	unsigned short x = 0x12345678;
	short x2 = *((char*)&x);
	if (*((char*)&x) == 0x12)
		std::cout << "大端" << std::endl;
	else
		std::cout << "小端" << std::endl;

	//通过函数 可以进行相应的转换
	unsigned short temp = htons(x);
	std::cout.setf(std::ios::hex);
	std::cout << temp <<  std::endl;

}

TCP客户端

// Hello_Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>
#include <winsock2.h>
#include <stdio.h>
#include <string>


void ErrorHandling(const char *message);

int main()
{
	WSADATA wsaData;

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() Error");
		return EXIT_FAILURE;
	}
	std::cout << "输入客户端的IP地址:";
	std::string strIP = " ";
	std::getline(std::cin, strIP);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	addrSrv.sin_addr.S_un.S_addr = inet_addr(strIP.c_str());

	//Step1 创建套接字
	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
	if (SOCKET_ERROR == sockClient)
	{
		ErrorHandling("socket() Error");
		return EXIT_FAILURE;
	}

	char buff[1024] = { 0 };
	//Step2 向服务器发出连接请求
	if (connect(sockClient, (struct  sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET)
	{
		ErrorHandling("connect() Error");
		return EXIT_FAILURE;
	}
	
	while (1)
	{
		//Step3 发送数据
		std::string message = " ";
		std::cout << "发送给服务端的消息:";
		std::getline(std::cin, message);
		
		send(sockClient, message.c_str(), message.length(), 0);

		if (message[0] == '+')
		{
			//接收数据
			recv(sockClient, buff, sizeof(buff), 0);
			std::cout << "接收服务端的消息:" << buff << std::endl;
			break;
		}
		

		memset(buff, 0, sizeof(buff));
		std::cout << std::endl;
		if (buff == "exit" || "exit" == message)
		{
			break;
		}

	}
	
	//关闭套接字
	closesocket(sockClient);
	WSACleanup();
	
	system("pause");
	return EXIT_SUCCESS;
}

void ErrorHandling(const char * message)
{
	std::cout << message << std::endl;
}

下面是UDP的例子

UDP服务端

// UDP_server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
#include <string>

#define BUF_SIZE	30

void ErrorHandling(const char *message);

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() Error");
		return EXIT_FAILURE;
	}

	//Step1 创建套接字
	SOCKET servSock = socket(PF_INET, SOCK_DGRAM, 0);
	if (servSock == INVALID_SOCKET)
	{
		ErrorHandling("UDP socket creation error");
		return EXIT_FAILURE;
	}

	SOCKADDR_IN	servAdr, clntAdr;
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(8888);

	if (bind(servSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() Error");
	}

	int clntAdrSz = 0;
	int strLen = 0;
	char message[BUF_SIZE] = { 0 };
	while (1)
	{
		clntAdrSz = sizeof(clntAdr);
		strLen = recvfrom(servSock, message, BUF_SIZE, 0, (SOCKADDR*)&clntAdr, &clntAdrSz);
		sendto(servSock, message, strLen, 0, (SOCKADDR*)&clntAdr, sizeof(clntAdr));
		std::cout << message << std::endl;
	}

	closesocket(servSock);
	WSACleanup();

	system("pause");
	return EXIT_SUCCESS;
}

void ErrorHandling(const char * message)
{
	std::cout << message << std::endl;
}

UDP客户端

// UDP_Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
#include <string>

#define BUF_SIZE	30

void ErrorHandling(const char *message);

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() Error");
		return EXIT_FAILURE;
	}

	//Step1 创建套接字
	SOCKET clientSock = socket(PF_INET, SOCK_DGRAM, 0);
	if (clientSock == INVALID_SOCKET)
	{
		ErrorHandling("UDP socket creation error");
		return EXIT_FAILURE;
	}

	SOCKADDR_IN	servAdr, clntAdr;
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servAdr.sin_port = htons(8888);
	connect(clientSock, (SOCKADDR*)&servAdr, sizeof(servAdr));
	

	int clntAdrSz = 0;
	int strLen = 0;
	char messageRecv[BUF_SIZE] = { 0 };
	std::string message = " ";
	while (1)
	{
		std::cout << "输入消息:";
		std::getline(std::cin, message);

		send(clientSock, message.c_str(), message.length(), 0);
		strLen = recv(clientSock, messageRecv, BUF_SIZE, 0);
		std::cout << "输出从服务端传送过来的数据:" << messageRecv << std::endl;
		//sendto(clientSock, message, strLen, 0, (SOCKADDR*)&clntAdr, sizeof(clntAdr));
		memset(messageRecv, 0, BUF_SIZE);
		if ("exit" == message)
		{
			break;
		}
	}

	closesocket(clientSock);
	WSACleanup();

	system("pause");
	return EXIT_SUCCESS;
}

void ErrorHandling(const char * message)
{
	std::cout << message << std::endl;
}

知识整理来自书籍《《TCP IP网络编程》.((韩)尹圣雨)》

猜你喜欢

转载自blog.csdn.net/Wuzm_/article/details/84073888