NAT映射
一般来说:源主机和目的主机都属于局域网,也就是ip地址可能相同
但是:路由器一般是公网ip,即整个网络环境可见
每一个路由器会维护一个NAT映射表
- 路由器的ip一般是固定的公网ip
- 路由器与把与它相连的主机:
私有ip+端口号
映射成路由器ip+端口号
此时再进行数据传输时:会用映射后的ip作为源地址,也就是保护了私有ip
打洞机制
首先进行通信的进程,需要经过路由器到达提供服务的服务器,然后服务器进行拆包后,帮助完成一次转发,再经过多跳路由器后,将信息传递给目的ip
也就是传输的数据包必须途径服务器
由于路由器具有保护机制-----第一次给路由器发送数据包的ip,会被路由器丢弃或屏蔽(防止恶意攻击)
所以,相互通信的ip,要被路由器识别为熟悉的ip才可以直接通信
- 主机和提供服务的服务器之间是可以直接通信的
- 所以服务器在请求服务时候,会在两台想建立通信的主机之间,把其相连的路由器标记为熟悉
- 这样服务器通过打洞机制就相当于在两台主机间建立了通路
几种访问机制(注意不是局域网(内网)之间)
- 公网ip和公网ip之间进行通信,直接访问
- 公网ip和私网ip之间进行通信,路由器要将私网ip进行NAT映射后,进行通信
- 私网ip和私网ip之间进行通信,公有服务器对相连服路由器进行打洞机制,然后通信
套接字
- Socket本身有“插座”的意思
- 在Linux环境下,用于表示
进程间网络通信
的特殊文件类型
- 本质为内核借助缓冲区形成的伪文件
注意socket一定有两端,成对出现:
数据发送端和数据接收端
就像插电,既要有插头有要求插排
IP地址:在网络环境中,唯一标识一台主机
端口号:主机中唯一标识一个进程
IP+port:在网络环境中,唯一标识一个进程,把这个叫成socket
管道
管道一定有两端,一端只能读,一端只能写,每端分别用一个文件描述符指向
数据只能从写段流入,从读端流出
socket
每次创建一个socket,都有一个文件描述符指向,但是这一个文件描述符由两个缓冲区指向
一个缓冲区用于读数据,一个缓冲区用于写数据,也就是全双工
两个socket是通过ip+port进行连接的
网络字节序
字节序:
-
也就是字节序
-
如果数据大于一个字节,在内存中存放是有顺序的
一般有两种:
- 小端字节序:将低位字节序存储在低位
- 大端字节序:将高位字节序存储在低位
其中:TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
但是,主机中采取的小端字节序
小端存,大端传,所以需要字节序的转换
假设内存:1000 1001 1002 1003 1004
现在的变量:int a=0x12345678(注意位次,←依次升高)
如果采用小端法存储(低位存低,高位存高)
12是高位存1004,依次:34存1003.。。。。
如果大端就是反过来(78存1004)
网络字节序和主机字节序的转换
#include <arpa/inet.h>
//host to network
//long转IP地址;short转端口号
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
//network to host
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
使用
#include<arpa/inet.h>
int main(){
int a=0x01020304;
short int b=0x0102;
//long转IP地址;short转端口号
//uint32_t htonl(uint32_t hostlong);
//uint16_t htons(uint16_t hostshort);
printf("htonl(0x%08x)=0x%08x\n",a,htonl(a));
//08表示输出8位,x表示16进制
printf("htons(0x%08x)=0x%08x\n",b,htonl(b));
return 0;
}
guojiawei@ubantu-gjw:~/Desktop$ gcc mm.c
guojiawei@ubantu-gjw:~/Desktop$ ./a.out
htonl(0x01020304)=0x04030201
htons(0x00000102)=0x02010000
ip地址转换
#include <arpa/inet.h>
//ip(点分十进制) to net(网络字节序)
int inet_pton(int af, const char *src, void *dst);
/*
af版本号---只有:AF_INET 和 AF_INET6 两种
src---点分十进制的数值
dst----转换后的整数的地址
*/
#include <arpa/inet.h>
//net to ip(字符串)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//dst----点分十进制数串
//size---缓存区的大小
实现:
#include<stdio.h>
#include<arpa/inet.h>
int main(){
char ip_str[]="172.20.226.11";
unsigned int ip_uint=0;
inet_pton(AF_INET,ip_str,&ip_uint);//注意是地址
printf("ip_uint=%d\n",ip_uint);
unsigned char* ip_p=NULL;
ip_p=(unsigned char*)&ip_uint;
printf("ip_uint=%d.%d.%d.%d\n",*ip_p,*(ip_p+1),\
*(ip_p+2),*(ip_p+3) );
return 0;
}
#include<stdio.h>
#include<arpa/inet.h>
int main(){
char ip_str[16]={0};
unsigned char ip[]={172,20,223,75};
inet_ntop(AF_INET,(unsigned int*)ip,ip_str,16);
printf("ip_str=%s\n",ip_str);
return 0;
}
//ip_str=172.20.223.75
套接字地址数据结构
sockaddr结构体-----用于描述IPv4协议的地址结构
- struct sockaddr由于不细致,现在已经不能用了
- 可是基于sockaddr细分的sockaddr_in无法直接作为socket的网络API的参数,所以需要强制类型转换
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];}
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
//man 7 ip
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */};
//由于历史遗留,只有一个成员,每次需要.使用
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */ 地址结构类型
__be16 sin_port; /* Port number */ 端口号
struct in_addr sin_addr; /* Internet address */ IP地址
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};//详细
使用:
struct sockaddr_in addr;
addr.sin_family=AF_INET//AF_INET6
addr.sin_port=htonl//ntohs
addr.sin_addr.s_addr=htol;ntosl;inet_ptonl;inrt_ntop;
网络套接字函数
socket()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
*/
/*type:
1:SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
2:SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
3:SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
*/
/*protocol:
传0 表示使用默认协议,系统自动选择
IPPROTO_TCP;IPPROTO_UDP 指定传输tcp或者udp
*/
int socket(int domain, int type, int protocol);
//成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
使用
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
int socketFd=0;
//创建:使用IPV4地址协议的TCP流式套接字
socketFd=socket(AF_INET,SOCK_STREAM,0);
if(socketFd==-1){
perror("socket()");
exit(-1);
}
else{
printf("socket has created successfully\n");
}
return 0;
}
bind()
#include <sys/types.h>
#include <sys/socket.h>
/*
sockfd: socket文件描述符
addr: 构造出IP地址加端口号
addrlen: sizeof(addr)长度
*/
int bind(int sockfd, const struct sockaddr *addr,\
socklen_t addrlen);
//返回值:0表示成功;-1表示失败
bind就是绑定,往套接字上绑定ip和端口号
由于传指针的地址,还得加上大小
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
int main(){
int socketFd=0;
//创建:使用IPV4地址协议的TCP流式套接字
socketFd=socket(AF_INET,SOCK_STREAM,0);
/*struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
struct in_addr { uint32_t s_addr;}
};*/
struct sockaddr_in my_addr;
//bzero函数是c++ string.h中的函数。
//描述:置字节字符串前n个字节为零且包括‘\0’
bzero(&my_addr,sizeof(my_addr));
my_addr.sin_family=AF_INET;
unsigned short port=8000;
my_addr.sin_port=htons(port);//hton:转换成网络字节序
my_addr.sin_addr.s_addr=htons(INADDR_ANY);
int res_bind=bind(socketFd,(struct sockaddr*)&my_addr,sizeof(my_addr));
printf("res=%d\n",res_bind);
return 0;
}
listen()
#include <sys/types.h>
#include <sys/socket.h>
/*backlog:
排队建立3次握手队列和刚刚建立3次握手队列的链接数和
也就是内核为此套接字建立的最大连接数(同时允许多少个用户端建立连接)
*/
int listen(int sockfd, int backlog);
listen()仅被TCP通信服务器程序调用,实现监听服务
- backlog—注意不是指定连接上限数,而是可以建立连接的
accept()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
三方握手完成后
,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
socklen_t *addrlen:
the caller must initialize it to contain the size (in bytes) of the structure pointed to by addr; on return it will contain the actual size of the peer address.
connect()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
*/
int connect(int sockfd, const struct sockaddr *addr,\
socklen_t addrlen);
//成功返回0,失败返回-1,设置errno
客户端
需要调用connect()连接服务器
,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
socket套接字流程图
比较典型的流程图
实现功能:客户端给服务器发送小写字母,服务器接收后转换成大写字母回传给客户端,然后客户端把内容显示到屏幕上
服务器端:
- socket创建一个网络套接字
- bind函数绑定ip地址和端口号(struct sockaddr_in)
- listen函数来限定同时发起服务器连接的数量
- accept含有阻塞功能,直到收到了用户的消息才连接
客户端:
- socket必须绑定ip和端口号才能访问
- 但是不用bind,会被自动分配,但是服务器端不可以自动分配,客户端无所谓
- connect进行连接:连接服务器的ip和端口号
客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>//大小写转换
#define SERV_PORT 6667//端口号
int main(void){
int sfd;
/*定义accept的客户端的参数*/
int cfd;
char buf[100]={0};
struct sockaddr_in cline_addr;
socklen_t clin_addr_len;
//sockaddr_in结构体定义在头文件:<arpa/inet.h>
struct sockaddr_in serv_addr;
sfd=socket(AF_INET,SOCK_STREAM,0);
/*定义结构体*/
serv_addr.sin_family=AF_INET;
//大小端问题:需要hton l是ip;s是端口号
serv_addr.sin_port=htons(SERV_PORT);
/*简单方法--------htonl(INADDR_ANY)*/
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
bind(sfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
/*listen*/
listen(sfd,32);//最大上限28个客户端
/*accept---接收一个文件描述符作为参数,最后返回一个文件描述符*/
clin_addr_len=sizeof(cline_addr);//注意第三个参数:&
cfd=accept(sfd,(struct sockaddr*)&cline_addr,&clin_addr_len);
/*连接后:开始读数据*/
int n=read(cfd,buf,sizeof(buf));//从用户端读
int i;
for(i=0;i<n;i++){
/*小写变大写*/
buf[i]=toupper(buf[i]);
}
write(cfd,buf,n);//写回给数据,别用sizeof
close(sfd);
close(cfd);
return 0;
}
模拟客户端与服务端的通信
//模拟客户端
$ nc -l 端口号
//模拟服务端
$ nc ip地址 端口号
程序运行结果
hello world
HELLO WORLD--------通信前提是等待,不能一下子就结束