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