网络分层模型
- 这一部分涉及内容比较多,分享一个链接,内容通俗易懂,写得很不错,各位可以先去看看,大概了解了解
互联网协议入门(通俗易懂的网络协议层次结构讲解)
预备知识
- 网络协议
了解一些基本的网络协议,比如以太网协议,TCP/IP协议,DNS协议,ARP协议等等,这些内容可以看上面给的链接 - 套接口
在linux中,套接口即主机+端口。说白了,客户端和服务器要想相互通讯,总需要知道对方的地址吧。怎么表示这个地址呢,就是用套接字。
常见的套接口地址有:通用套接口地址和ipv4套接口地址。
通用套接口地址:
顾名思义,该地址是通用的,所有的套接口地址都可以转为通用套接口地址
struct sockaddr
{
unit8_t sin_len; //4字节
sa_fmily_t sa_family; //4字节
char sa_data[14]; //14字节
}
ipv4套接口地址:
该套接口地址是给ipv4用的,当然还有其他套接口地址,只不过ipv4在学习中是常用的。
struct sockaddr_in {
uint8_t sin_len; //整个sockaddr_in结构体的长度
sa_family_t sin_family; //指定该地址家族,在这里必须设为AF_INET(ipv4)
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IPV4的地址
char sin_zero[8]; //暂不使用,一般将其设置为0
};
可以发现:通用套接口地址结构和ipv4套接口地址结构,他们大小都是相等的。通用套接口地址中的char sa_data[14],
就相当于ipv4套接口地址结构中的in_port_t sin_port 和 struct in_addr sin_addr 和 char sin_zero[8]。现在,你可以想明白为什么叫通用套接口地址了吧。
- 字节序
大端字节序(Big Endian)
大端字节序又叫大端对齐,即高字节放在低地址,低字节放在高地址
小端字节序(Little Endian)
小端字节序又叫小端对齐,即高字节放在高地址,低字节放在低地址
主机字节序
不同的主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。
网络字节序
网络字节序规定为大端字节序
字节序带来的问题
我们知道:有些机器是大端对齐,有些是小端对齐。那么,如果一个大端对齐的机器给小端对齐的机器传数据,必将导致错误。所以,这时候我们需要字节序转换函数。在本地设备中,先把本机的字节序转为网络字节序传到服务器,接受方再将网络字节序转为自己的字节序,这样子就保证了不会发生错误。
字节序转换函数
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
说明:在上述的函数中,h代表host;n代表network s代表short;l代表long - 地址转换函数
为什么要有地址转换函数
ip地址是32位的,我们为了表示方便,才写成了十进制点的模式
所以我们要将ip地质的十进制点模式转为32位的二进制模式
这样子服务器才会知道ip地址是多少
地址转换函数
int inet_aton(const char *cp, struct in_addr *inp); //第一种地址转换函数
struct in_addr{
u_int32_t s_addr;
}
in_addr_t inet_addr(const char *cp); //第二种地址转换函数
char *inet_ntoa(struct in_addr in); //第三种地址转换函数 - 套接字类型
流式套接字(SOCK_STREAM) //TCP协议
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
数据报式套接字(SOCK_DGRAM) //UDP协议
提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。
原始套接字(SOCK_RAW) - 字节序和地址转换函数举例
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//字节序转换函数
int main01()
{
unsigned int data = 0x12345678;
char *p = &data;
printf("%x, %x, %x \n", p[0], p[1], p[2], p[3]);
if (p[0] == 0x78)
{
printf("small \n");
}
else
{
printf("big \n");
}
/***************************************
输出:
78,56,34
small
说明是小端对齐,低字节放在低地址
x86为小段模式
***************************************/
uint32_t mynetdat = htonl(data); //将主机字节序转为网络字节序
p = &mynetdat;
printf("%x, %x, %x \n", p[0], p[1], p[2], p[3]);
if (p[0] == 0x78)
{
printf("small \n");
}
else
{
printf("big \n");
}
/***************************************
输出:
12,34,56
big
说明是大端对齐,低字节放在高地址
网络字节序规定为大端字节序
***************************************/
return 0;
}
/***************************************
为什么要有字节序转换函数:
我们知道:有些机器是大端对齐,有些是小端对齐。
那么,如果一个大端对齐的机器给小端对齐的机器传数据,必将导致错误。
所以,这时候我们需要字节序转换函数
在本地设备中,先把本机的字节序转为网络字节序传到服务器,
如何接受方再将网络字节序转为自己的字节序,这样子就保证了不会发生错误
***************************************/
//地址转换函数
int main()
{
//int inet_aton(const char *cp, struct in_addr *inp);
//in_addr_t inet_addr(const char *cp);
//char *inet_ntoa(struct in_addr in);
in_addr_t myint = inet_addr("192.168.6.222");
printf("%d\n", myint);
/*
struct in_addr{
u_int32_t s_addr;
}
*/
struct in_addr myaddr;
inet_aton("192.168.6.222", &myaddr);
printf("%d\n", myaddr.s_addr);
printf("%s\n", inet_ntoa(myaddr));
return 0;
}
/***************************************
为什么要有地址转换函数 :
tcp协议是32位的
所以我们要将ip地质的十进制点模式转为32位的二进制模式
这样子服务器才会知道ip地址是多少
***************************************/
SocketApi基本编程模型
CS模型(客户端client/服务器service模式)
客户和服务器之间要想相互通讯,首先要搭建环境咯,如何搭建呢?
第一步
服务器端:socket():这个函数意思是创建一个监听套接字。相当于在家安装电话,等朋友打过来。
客户端:socket():客户端也有创建一个套接字。相当于在家安装电话,以便可以给朋友打电话。
第二步
服务器端:bind():bind函数是用与绑定端口,端口是什么上面链接有介绍。相当于我有那么多朋友,我当然要知道我在等哪个朋友电话咯。
客户端:不需要做什么
第三步
服务器端:listen():listen函数用来监听连接请求。相当于时刻等待电话响起。
客户端:不需要做什么
第四步
服务器端:accept():accept函数用来生成连接套接字,即主动套接字,这个套接字用来进行收发数据。如果此时客户端没有尝试连接(即调用connect函数),那么服务器端将处于阻塞状态。相当于电话响了,我要拿起电话来接听。
客户端:connect():connect函数用来发送连接请求。相当于给朋友打电话。
以上四部完成时,环境搭建完毕
第五步
服务器端:read(), write():读数据写数据
客户端:read(), write():读数据写数据
第六步
服务器端:关闭监听套接字和关闭主动套接字
客户端:关闭套接字简单服务器模型
协议族和套接字类型
SocketAPI函数
服务器端:
#include <sys/socket.h>
int socket(int domain , int type, int protocol);
功能:
创建一个套接字用于通信,被动套接字(监听套接字)
返回值:
成功返回非负整数, 它与文件描述符类似,
我们把它称为套接口描述字,简称套接字。失败返回-1
参数:
domain :指定通信协议族(protocol family)(ipv4,ipv6等等)
type:指定socket类型,流式套接字SOCK_STREAM(TCP协议),数据报套接字SOCK_DGRAM(UDP协议),原始套接字SOCK_RAW
protocol :协议类型(一般填0)
Example:
socket(PF_INET, SOCK_STREAM, 0); //ipv4,TCP协议,0
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
功能:
设置地址复用,使服务器关闭,客户端未关闭时,可以重启服务器
若没设置地址复用,则服务器关闭,客户端未关闭时,服务器不能重启,必须全部关闭服务器和客户端,然后重新连接
一般放在socket函数之后,bind函数之前
返回值:
成功返回0,不成功返回-1
参数:
sockfd:返回的套接字
level:一般是填SOL_SOCKET
optname:一般是填SO_REUSEADDR
Example:
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
绑定一个本地地址到套接字
返回值:
成功返回0,失败返回-1
参数:
sockfd:socket函数返回的套接字
addr:要绑定的地址
//通用套接口地址结构
struct sockaddr
{
unit8_t sin_len;
sa_fmily_t sa_family;
char sa_data[14];
}
//IPv4套接口地址结构 具体请查看:man 7 ip
struct sockaddr_in {
uint8_t sin_len; //整个sockaddr_in结构体的长度
sa_family_t sin_family; //指定该地址家族,在这里必须设为AF_INET(ipv4)
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IPV4的地址
char sin_zero[8]; //暂不使用,一般将其设置为0
};
//IP地址
struct in_addr {
u_int32_t s_addr; //IP地址,32位
};
addrlen:地址长度
Example:
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr(“172.0.0.1”);
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if ( bind(sockfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0 )
{
perror("func bind: ");
exit(0);
}
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:
监听
返回值:
成功返回0,不成功返回-1
参数:
sockfd:socket函数返回的套接字
backlog: 内核规定的套接字的最大连接个数
进程正在处理一个连接请求的时候,可能还存在其它的连接请求。
因为TCP连接是一个过程,所以可能存在一种半连接的状态,
有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。
如果这个情况出现了,服务器进程希望内核如何处理呢?
内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接
但服务器进程还没有接手处理的连接(还没有调用accept函数的连接),
这样的一个队列内核不可能让其任意大,
所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
对于给定的监听套接字,内核要维护两个队列:
1 已有客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。
2 已完成连接的队列。
Example:
listen(sockfd, SOMAXCONN);
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
返回值:
成功返回非负整数,是一个描述符,这个描述即生成的主动套接字
失败返回-1
参数:
sockfd:服务器套接字,socket返回的套接口描述字
addr:将返回对等方的套接字地址
addrlen:返回对等方的套接字地址长度
Example:
struct sockaddr_in perraddr;
socklen_t perrlen;
perrlen = sizeof(perrlen);
accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);
客户端:
#include <sys/socket.h>
int socket(int domain , int type, int protocol);
功能:
创建一个套接字用于通信,被动套接字(监听套接字)
返回值:
成功返回非负整数, 它与文件描述符类似,
我们把它称为套接口描述字,简称套接字。失败返回-1
参数:
domain :指定通信协议族(protocol family)(ipv4,ipv6等等)
type:指定socket类型,流式套接字SOCK_STREAM(TCP协议),数据报套接字SOCK_DGRAM(UDP协议),原始套接字SOCK_RAW
protocol :协议类型(一般填0)
Example:
socket(PF_INET, SOCK_STREAM, 0); //ipv4,TCP协议,0
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
建立一个连接至addr所指定的套接字
返回值:
成功返回0,失败返回-1
参数:
sockfd:未连接套接字
addr:要连接的套接字地址
addrlen:第二个参数addr长度
Example:
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr(“172.0.0.1”);
connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr));
下面请看具体例子
服务器端:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
//创建监听套接字
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}
//绑定端口号
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if ( bind(sockfd, (const struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func bind: ");
exit(0);
}
//设置监听
//一旦调用了listen函数,这个套接字sockfd将变成被动套接字,只能接受,不能发送消息
//listen管理了两个队列
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("func listen: ");
exit(0);
}
//等待接受,完成连接
struct sockaddr_in perraddr;
socklen_t perrlen;
perrlen = sizeof(perrlen);
unsigned int conn;
conn = accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);//accept返回一个新的连接,这个连接时一个主动套接字
if (conn == -1)
{
perror("func accept: ");
exit(0);
}
printf("perradd:%s, perrport:%d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port)); //打印客户端ip地址,端口号
// 收发数据
char revbuf[1024] = {0};
while (1)
{
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcp/ip协议会返回一个数据包
printf("client already close\n");
exit(0);
}
else if (ret < 0)
{
perror("read fail:");
exit(0);
}
fputs(revbuf, stdout); //服务器端打印收到的数据
write(conn, revbuf, ret); //服务器端回发报文
memset(revbuf, 0, sizeof(revbuf));
}
close(conn);
close(sockfd);
return 0;
}
客户端
int main()
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if ( connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func connect: ");
exit(0);
}
char revbuf[1024] = {0};
char sendbuf[1024] = {0};
while( fgets(sendbuf, sizeof(sendbuf), stdin) != NULL )
{
write(sockfd, sendbuf, strlen(sendbuf)); //向服务器写数据
read(sockfd, revbuf, sizeof(revbuf)); //从服务器读数据
fputs(revbuf, stdout);
memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sockfd);
return 0;
}
为了实现多用户使用服务器,我们可以用fork方法,每连接一次便fork一次,让一个进程去管理一个用户,示例如下:
服务器端:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
//创建监听套接字
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}
//绑定端口号
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if ( bind(sockfd, (const struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func bind: ");
exit(0);
}
//设置监听
//一旦调用了listen函数,这个套接字sockfd将变成被动套接字,只能接受,不能发送消息
//listen管理了两个队列
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("func listen: ");
exit(0);
}
//等待接受,完成连接
// 收发数据
struct sockaddr_in perraddr;
socklen_t perrlen;
perrlen = sizeof(perrlen);
unsigned int conn;
while (1)
{
conn = accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);//accept返回一个新的连接,这个连接时一个主动套接字
if (conn == -1)
{
perror("func accept: ");
exit(0);
}
printf("perradd:%s, perrport:%d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port)); //打印客户端ip地址,端口号
//每来一个连接,fork一次
int pid = fork();
if (pid == 0) //子进程用于收发数据
{
while(1)
{
close(sockfd); //子进程不需要监听
char revbuf[1024] = {0};
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcp/ip协议会返回一个数据包
printf("client already close\n");
exit(0);
}
else if (ret < 0)
{
perror("read fail:");
exit(0);
}
fputs(revbuf, stdout); //服务器端打印收到的数据
write(conn, revbuf, ret); //服务器端回发报文
memset(revbuf, 0, sizeof(revbuf));
}
}
else if (pid > 0) //父进程只用于监听,关闭主动套接字
{
close(conn);
}
else
{
printf("fork fail\n");
close(conn);
close(sockfd);
exit(0);
}
}
return 0;
}
客户端
int main()
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
if ( connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func connect: ");
exit(0);
}
char revbuf[1024] = {0};
char sendbuf[1024] = {0};
while( fgets(sendbuf, sizeof(sendbuf), stdin) != NULL )
{
write(sockfd, sendbuf, strlen(sendbuf)); //向服务器写数据
read(sockfd, revbuf, sizeof(revbuf)); //从服务器读数据
fputs(revbuf, stdout);
memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sockfd);
return 0;
}