网络编程:写一个简单的网络通讯模型之前应该知道( socket函数 主机字节序和网络字节序 相关函数参数用到的结构体解读)

一.socket函数

1.什么是协议?

来自百度:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。 在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。

2.创建socket

函数定义:

int socket(int domain, int type, int protocol);

1)参数1 domain 通讯的协议家族
PF_INET IPv4 互联网协议族。
PF_INET6 IPv6 互联网协议族。
PF_LOCAL 本地通信的协议族。
PF_PACKET 内核底层的协议族。
PF_IPX IPX Novell 协议族。
IPv6 尚未普及,其它的不常用,主要是IPv4。

2)参数2 type 数据传输的类型
SOCK_STREAM 面向连接的 socket:1)数据不会丢失;2)数据的顺序不会错乱;3)双向通
道。
SOCK_DGRAM   无连接的 socket:1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输
的效率更高。

3)参数3 protocol 最终使用的协议
在 IPv4 网络协议家族中,数据传输方式为 SOCK_STREAM 的协议只有 IPPROTO_TCP,数据传输方式为 SOCK_DGRAM 的协议只有 IPPROTO_UDP
本参数也可以填 0。
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建 tcp 的 sock
socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); // 创建 udp 的 sock

4)返回值

成功返回一个有效的 socket,失败返回-1,errno 被设置。
全部网络编程的函数,失败时基本上都是返回-1,errno 被设置。
只要参数没填错,基本上不会失败。
不过,单个进程中创建的 socket 数量与受系统参数 open files 的限制。(ulimit -a命令 可以查看最大openfile数一般是是1024,创建太多会出现"too many file"的提示)。

3.TCP和UDP

1)TCP和UDP的区别

TCP
a)TCP 面向连接,通过三次握手建立连接,四次挥手断开连接;重点
b)TCP 是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重
复,并且按序到达;
c)TCP 把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题;
d)TCP 只支持点对点通信;
e)TCP 报文的首部较大,为 20 字节;
f)TCP 是全双工的可靠信道。
UDP
a)UDP 是无连接的,即发送数据之前不需要建立连接,这种方式为 UDP 带来了高效的传输效率,
但也导致无法确保数据的发送成功;
b)UDP 以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题;
c)UDP 没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率;
d)UDP 支持一对一,一对多,多对一和多对多的通信;
e)UDP 报文的首部比较小,只有 8 字节;
f)UDP 是不可靠信道。

2)TCP保证自身可靠的方法

a)数据分片:在发送端对用户数据进行分片,在接收端进行重组,由 TCP 确定分片的大小并控制
分片和重组;
b)到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包;
c)超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片;d)滑动窗口:TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该
发送多少字节的数据。当滑动窗口为 0 时,发送方不会再发送数据;
e)失序处理:TCP 的接收端会把接收到的数据重新排序;
f)重复处理:如果传输的分片出现重复,TCP 的接收端会丢弃重复的数据;
g)数据校验:TCP 通过数据的检验和来判断数据在传输过程中是否正确。

3)UDP 不可靠的原因

没有上述 TCP 的机制,如果校验出错,UDP 会将该报文丢弃。

4)TCP和UDP的使用场景

TCP使用场景:TCP 实现了数据传输过程中的各种控制,适合对可靠性有要求的场景。比如文件、数据包下载等。
UDP 使用场景:
可以容忍数据丢失的场景:
 视频、音频等多媒体通信(即时通信);
 广播信息。

二.主机字节序和网络字节序

1.大端序/小端序

如果数据类型占用的内存空间大于 1 字节,CPU 把数据存放在内存中的方式有两种:

* 大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。
* 小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。


假设从内存地址 0x00000001 处开始存储十六进制数 0x12345678,那么:
Bit-endian(按原来顺序存储)
0x00000001 0x12
0x00000002 0x34
0x00000003 0x56
0x00000004 0x78

对于内存来说,0x00000001是低位开始,到02、03... 0x12345678十六进制数是以78为低位开始到56、34、12高位。
Little-endian(颠倒顺序储存)
0x00000001 0x78
0x00000002 0x56
0x00000003 0x34
0x00000004 0x12

Intel 系列的 CPU 以小端序方式保存数据,其它型号的 CPU 不一定。
操作文件的本质是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件
(socket 也是文件描述符)。
这样的话,字节序不同的计算机之间传输数据,可能会出现问题。

网络编程:写一个简单的网络通讯模型之前应该知道( socket函数 主机字节序和网络字节序 相关函数参数用到的结构体解读)_第1张图片

2.网络字节序

为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。
C 语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:
uint16_t h to n s(uint16_t hostshort); // uint16_t 2 字节的整数 unsigned short
uint32_t htonl(uint32_t hostlong); // uint32_t 4 字节的整数 unsigned int

uint16_t ntohs(uint16_t netshort);
uint32_t n to h l(uint32_t netlong);

起名还是挺规律的:
h host(主机);
to 转换;
n network(网络);
s short(2 字节,16 位的整数);
l long(4 字节,32 位的整数);

3.IP地址和通讯端口

在计算机中,IPv4 的地址用 4 字节的整数存放,通讯端口用 2 字节的整数(0-65535)存放
例如:字符串形式:192.168.190.134   255.255.255.255(每部分肯定不超过255)

 整数形式:3232284294

二进制形式:
           192           168           190          134
大端:11000000 10101000 10111110 10000110
小端:10000110 10111110 10101000 11000000

*为什么使用四字节整数存放地址?

如果用字符串要使用15字节,而整数只需要四字节,二进制同理。

4.如何处理大小端序

在网络编程中,数据收发的时候有自动转换机制,不需要手动转换,只有向 sockaddr_in 结
体成员变量填充数据
时,才需要考虑字节序的问题

三.相关结构体解读

1.sockaddr结构体

存放协议族端口和地址信息客户端的connect()函数服务端的 bind()函数需要这个结构体。
struct sockaddr {
unsigned short sa_family;
// 协议族,与 socket()函数的第一个参数相同,填 AF_INET。
unsigned char sa_data[14]; // 14 字节的端口和地址。
};

2.sockaddr_in结构体

sockaddr 结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的 sockaddr_in 结构体,它的大小与 sockaddr 相同,可以强制转换成 sockaddr。
struct sockaddr_in {
unsigned short sin_family;
// 协议族,与 socket()函数的第一个参数相同,填 AF_INET。
unsigned short sin_port; // 16 位端口号,大端序。用 htons(整数的端口)转换。
struct in_addr sin_addr; // IP 地址的结构体。
unsigned char sin_zero[8]; // 未使用,为了保持与 struct sockaddr 一样的长度而添加。
};
struct in_addr {
// IP 地址的结构体。
unsigned int s_addr; // 32 位的 IP 地址,大端序。
};

3.gethostbyname 函数

根据域名/主机名/字符串 IP 获取大端序 IP,用于网络通讯的客户端程序中。
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; // 主机名。
char **h_aliases; // 主机所有别名构成的字符串数组,同一 IP 可绑定多个域名。
short h_addrtype; // 主机 IP 地址的类型,例如 IPV4(AF_INET)还是 IPV6。
short h_length; // 主机 IP 地址长度,IPV4 地址为 4,IPV6 地址则为 16。
char **h_addr_list; // 主机的 ip 地址,以网络字节序存储。
};

#define h_addr h_addr_list[0] // for backward compatibility.
转换后,用以下代码把大端序的地址复制到 sockaddr_in 结构体的 sin_addr 成员中。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);

4.字符串 IP 与大端序 IP 的转换

C 语言提供了几个库函数,用于字符串格式的 IP 和大端序 IP 的互相转换,用于网络通讯的服务端
程序中。
typedef unsigned int in_addr_t; // 32 位大端序的 IP 地址。

// 把字符串格式的 IP 转换成大端序的 IP,转换后的 IP 赋给 sockaddr_in.in_addr.s_addr。
in_addr_t inet_addr(const char *cp);
// 把字符串格式的 IP 转换成大端序的 IP,转换后的 IP 将填充到 sockaddr_in.in_addr 成员。
int inet_aton(const char *cp, struct in_addr *inp);
// 把大端序 IP 转换成字符串格式的 IP,用于在服务端程序中解析客户端的 IP 地址。
char *inet_ntoa(struct in_addr in);

5.演示程序1

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
int main(int argc,char *argv[])
{
if (argc!=3)
{
cout << "Using:./demo5 服务端的 IP 服务端的端口\nExample:./demo5 192.168.101.138
5005\n\n";
return -1;
}
// 第 1 步:创建客户端的 socket。
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd==-1)
{
perror("socket"); return -1;
}
// 第 2 步:向服务器发起连接请求。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port = htons(atoi(argv[2])); // ②指定服务端的通信端口。
struct hostent* h; // 用于存放服务端 IP 地址(大端序)的结构体的指
针。
if ( (h = gethostbyname(argv[1])) == nullptr ) // 把域名/主机名/字符串格式的 IP 转换成结构
体。
{
cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1;
}
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的 IP(大端序)。
//servaddr.sin_addr.s_addr=inet_addr(argv[1]); // ③指定服务端的 IP,只能用 IP,不能用域名
和主机名。
if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1) // 向服务端发起
连接清求。
{
perror("connect"); close(sockfd); return -1;
}
// 第 3 步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一
个请求报文。
char buffer[1024];
for (int ii=0;ii<10;ii++) // 循环 3 次,将与服务端进行三次通讯。
{
int iret;
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"测试开始,收到请回复...",ii+1,ii+1); // 生成请求报文内容。
// 向服务端发送请求报文。
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
memset(buffer,0,sizeof(buffer));
// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
// 第 4 步:关闭 socket,释放资源。
close(sockfd);
}

6.演示程序2

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
int main(int argc,char *argv[])
{
if (argc!=2)
{
cout << "Using:./demo6 通讯端口\nExample:./demo6 5005\n\n"; // 端口大于 1024,不
与其它的重复。
cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通 5005 端口。\n";
cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";
return -1;
}
// 第 1 步:创建服务端的 socket。
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if (listenfd==-1)
{
perror("socket"); return -1;
}
// 第 2 步:把服务端用于通信的 IP 和端口绑定到 socket 上。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port=htons(atoi(argv[1])); // ②指定服务端的通信端口。
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个 IP,全部的 IP 都可
//以用于通讯。
//servaddr.sin_addr.s_addr=inet_addr("192.168.101.138"); // ③指定服务端用于通讯的 IP(大
端序)。
// 绑定服务端的 IP 和端口。
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
perror("bind"); close(listenfd); return -1;
}
// 第 3 步:把 socket 设置为可连接(监听)的状态。
if (listen(listenfd,5) == -1 )
{
perror("listen"); close(listenfd); return -1;
}
// 第 4 步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
int clientfd=accept(listenfd,0,0);
if (clientfd==-1)
{
perror("accept"); close(listenfd); return -1;
}
cout << "客户端已连接。\n";
// 第 5 步:与客户端通信,接收客户端发过来的报文后,回复 ok。
char buffer[1024];
while (true)
{
int iret;
memset(buffer,0,sizeof(buffer));
// 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。
// 如果客户端已断开连接,recv()函数将返回 0。
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
strcpy(buffer,"ok"); // 生成回应报文内容。
// 向客户端发送回应报文。
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
// 第 6 步:关闭 socket,释放资源。
close(listenfd); // 关闭服务端用于监听的 socket。
close(clientfd); // 关闭客户端连上来的 socket。
}

7.为什么客户端不用服务端的htons()函数

可以用但是客户端可访问服务端方式只剩下了字符串IP地址这一种方法,域名,网址,主机名等都用不了了。

四.关于更多

进阶请持续关注下一篇文章,关于客户端/服务端的封装,多进程的服务器,文件传输功能扩展,三次握手与四次挥手以及TCP缓存的内容。


        

你可能感兴趣的:(网络,tcp/ip,服务器,linux,c语言,c++)