socket基础api和服务端与客户端1.0
目录
socket基础api
Socket可以当成文件操作。
客户端中:
- 建立一个Socket相当于声明一个文件指针;
- 连接服务器相当于打开文件;
- 向服务器发送数据相当于fwrite文件,
- 接收服务端数据相当于fread文件;
- 关闭连接相当于关闭文件。
客户端通过IP地址和端口号定向连接到服务器。
服务端中:7步实现网络交互。与客户端不同的是,服务端需要绑定端口,监听端口并阻塞等待客户端连接。服务端共3步阻塞,需要搭配多线程编程实现相应功能。
#define WIN32_LEAN_AND_MEAN
#include<Windows.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库
int main()
{
//启动 windows socket 2.x 环境
WORD versionCode = MAKEWORD(2, 2); //创建一个版本号
WSADATA data;
WSAStartup(versionCode, &data); //启动Socket网络API的函数
///
///
WSACleanup();
return 0;
}
加依赖库的第二种方法
建立简易的TCP服务端和客户端
网络字节序和主机字节序
NBO : 网络字节序
HBO : 主机字节序
LE little-endian:小端
BE big-endian:大端
一、主机字节序:
自己的主机内部,内存中数据的处理方式,可以分为两种:
大端字节序:按照内存的增长方向,高位数据存储于低位内存中(最直观的字节序 )低地址高字节
小端字节序:按照内存的增长方向,高位数据存储于高位内存中(符合人的思维的字节序)低地址低字节
二、网络字节序(使用大端字节序):
网络数据流也有大小端之分。
网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,为了不使数据流乱序,接收主机也会把从网络上接收的数据按内存地址从低到高的顺序保存在接收缓冲区中。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
三、转换函数
将主机字节序转换为网络字节序
htons() : 由主机字节序转换为网络字节序的16位整数值。(host to net)
htonl () : 由主机字节序转换为网络字节序的32位整数值。
将网络字节序转换为主机字节序
ntohs() : 由网络字节序转换为主机字节序的16位整数值。
ntohl () : 由网络字节序转换为主机字节序的32位整数值。
主机字节序虽然有两种,但是我可以通过htons这样的函数,将其统一转换为网络字节序。
sockaddr和struct sockaddr_in
这两个都要求必须是网络字节序(NBO)
sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。这两个都是16比特,两个可以进行转换,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
sockaddr_in解决了这个缺陷,把port和addr 分开储存在两个变量中
二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化,指向sockaddr_in结构的指针也可以指向sockaddr。
- sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
- sockaddr_in 是internet环境下套接字的地址形式
在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数
inet_addr()和inet_ntoa()
1.把ip地址转化为用于网络传输的二进制数值
in_addr_t inet_addr(const char *cp);
inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序无符号二进制值,如果参数char *cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也会返回-1,255.255.255.255是一个有效的地址,不过inet_addr无法处理;
int inet_aton(const char *cp, struct in_addr *inp);
inet_aton() 转换网络主机地址ip(如192.168.1.10)为二进制数值,并存储在struct in_addr结构中,即第二个参数*inp,函数返回非0表示cp主机地址有效,返回0表示主机地址无效。(这个转换完后不能用于网络传输,还需要调用htons或htonl函数才能将主机字节顺序转化为网络字节顺序)
2.将网络传输的二进制数值转化为成点分十进制的ip地址
char *inet_ntoa(struct in_addr in);
inet_addr() 如果正确执行将返回一个无符号长整数型数。如果传入的字符串不是一个合法的IP地址(比如某个大于255),将返回INADDR_NONE。
inet_ntoa 函数转换网络字节排序的地址为标准的ASCII以点分开的地址,该函数返回指向点分开的字符串地址(如192.168.1.10)的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(覆盖),所以如果需要及时保存复制
新型网路地址转化函数inet_pton和inet_ntop
两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构的二进制值。
#include <arpe/inet.h>
//将点分十进制的ip地址转化为用于网络传输的数值格式
int inet_pton(int family, const char *strptr, void *addrptr);
返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
//将数值格式转化为点分十进制的ip地址格式
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
返回值:若成功则为指向结构的指针,若出错则为NULL
family参数代表ipv4还是ipv6
第一个函数尝试转换由strptr指针所指向的字符串,并通过addrptr指针存放二进制结果,若成功则返回值为1,否则如果所指定的family而言输入字符串不是有效的表达式格式,那么返回值为0.
第二个函数inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。如果len太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno为ENOSPC。
socket方法
socket方法有三个参数
第一个参数af
- AF_UNIX, AF_LOCAL 本地通信
- AF_INET IPv4网络通信
- AF_INET6 IPv6网络通信
- AF_PACKET 链路层通信
第二个参数type
Type就是socket的类型,对于AF_INET协议族而言有(常用的几个)
- 流套接字(SOCK_STREAM)
- 数据包套接字(SOCK_DGRAM)
- 原始套接字(SOCK_RAW)
第三个参数protocol
IPPROTO_TCP(tcp类型)
accept函数
accept(SOCKET s,struct sockaddr FAR * addr,int FAR * addrlen);
- 第一个参数用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字),
- 第二个参数是用来保存客户端套接字对应的“地址”(包括客户端IP和端口信息等)
- 第三个参数是“地址”的占地大小。返回值对应客户端套接字标识。
- 返回值是客户端套接字的标识(一个int类型的数字)
accept函数指定服务端去接受客户端的连接,接收后,返回了客户端套接字的标识,且获得了客户端套接字的“地方”(包括客户端IP和端口信息等)。
TCP服务端和客户端服务流程
(1) TCP服务端流程(简要版)
- 创建一个套接字
- bind,建立一个sockaddr_in,赋值ip的版本,ip,端口,将套接字和这个sockaddr_in绑定
- 监听网络端口 listen
- 等待接受客户端连接 accept
- 向客户端发送一条数据 send
- 关闭socke
TCP服务端流程(完整版 )
- 传递三个参数进行socket初始化创建一个套接字,
- 传递地址族,端口号,ip建立一个sockaddr_in对象与socket对象通过bind方法绑定,需要判断绑定端口成功与否
- 监听网络端口 listen,可指定连接最大值
- 将套接字传递给accept,当客户端接入时 会得到连入客户端的socket地址和长度
- 向客户端发送一条数据 send
- 关闭socke
(2) TCP客户端流程
- 建立一个Socket
- 连接服务器 connect
- 接受服务器信息 recive
- 关闭 socket
服务端代码
TCP服务端:新建一个项目,
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
#include<string>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库
int main()
{
//启动 windows socket 2.x 环境
WORD versionCode = MAKEWORD(2, 2); //创建一个版本号
WSADATA data;
WSAStartup(versionCode, &data); //启动Socket网络API的函数
///
//(1) 用Socket API建立简易的TCP服务端
// 1. 建立一个Socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // ipv4 面向字节流的 tcp协议
// 2. 绑定接受客户端连接的端口 bind
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567); //端口号 host to net sockaddr_in中port是USHORT类型
//网络中port是 unsigend short类型 因此需要Htons进行转换
//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器绑定的IP地址 127.0.0.1是本地地址
_sin.sin_addr.S_un.S_addr = INADDR_ANY; //不限定访问该服务端的IP
if (bind(_sock, (sockaddr*)&_sin, sizeof(_sin)) == SOCKET_ERROR) //sockaddr 不利于编码
{
cout << "ERROR: 绑定用于接受客户端连接的网络端口失败..." << endl;
}
else
{
cout << "SUCCESS: 绑定端口成功..." << endl;
}
// 3. 监听网络端口 listen
if (listen(_sock, 5) == SOCKET_ERROR)//第二个参数 backbag 最大允许连接数量
{
cout << "ERROR: 监听用于接受客户端连接的网络端口失败..." << endl;
}
else
{
cout << "SUCCESS: 监听端口成功..." << endl;
}
// 4. 等待接受客户端连接 accept
sockaddr_in _clientAddr = {};
int cliendAddrLen = sizeof(_clientAddr);
SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端
char msgBuf[] = "Hello, I'm Server";
while (true)
{ //当客户端接入时 会得到连入客户端的socket地址和长度
_clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen);
if (INVALID_SOCKET == _clientSock) //接受到无效接入
{
cout << "ERROR: 接受到无效客户端SOCKET..." << endl;
}
else
{
cout << "新Client加入: IP = " << inet_ntoa(_clientAddr.sin_addr) << endl;
//inet_ntoa 将ip地址转换成可读的字符串
// 5. 向客户端发送一条数据 send
send(_clientSock, msgBuf, strlen(msgBuf) + 1, 0); //+1是为了把\0算进去
}
}
// 6. 关闭socket
closesocket(_sock);
// 清除Windows socket环境
WSACleanup();
return 0;
}
客户端
客户端的连接步骤
- 建立一个Socket
- 连接服务器 connect
- 接受服务器信息 recive
- 关闭 socket
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
#include<string>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库
int main()
{
//启动 windows socket 2.x 环境
WORD versionCode = MAKEWORD(2, 2); //创建一个版本号
WSADATA data;
WSAStartup(versionCode, &data); //启动Socket网络API的函数
///
//(1) 用Socket API建立简易的TCP客户端
// 1. 建立一个Socket 下面第三个参数不需要指定
SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0); // ipv4 面向字节流的 tcp协议
if (INVALID_SOCKET == _sock)
{
cout << "错误,建立socket失败"<<endl;
}
else
{
cout << "成功建立客户端socket"<<endl;
}
// 2. 连接服务器 connect
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567); //端口号 host to net sockaddr_in中port是USHORT类型
//网络中port是 unsigend short类型 因此需要Htons进行转换
//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器绑定的IP地址 127.0.0.1是本地地址
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
if (SOCKET_ERROR == ret)
{
cout << "错误,connect失败" << endl;
}
else
{
cout << "成功,connect 成功" << endl;
}
//3 接收服务器数据 resv
char recvBuf[256] = {};
int recvlen=recv(_sock, recvBuf, 256, 0);
if (recvlen > 0)
{
cout << "接收到数据:"<<recvBuf<<endl;
}
else
{
cout << "接收数据失败/n";
}
// 6. 关闭socket
closesocket(_sock);
// 清除Windows socket环境
WSACleanup();
getchar();//防止一闪而过
return 0;
}
下节服务端与客户端1.1——持续处理“单个客户端”发送的请求_贪睡的蜗牛的博客-CSDN博客
参考
sockaddr和sockaddr_in详解_爱橙子的OK绷的博客-CSDN博客_sockaddr_in