欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TCP/IP基础知识复习

程序员文章站 2022-03-30 17:09:36
...

/*
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网络编程》.((韩)尹圣雨)》

相关标签: 基础知识