一、网络基础
1、网络通信概念
网络通信,就是支持位于网络中不同主机上面的2个进程进行通信。
2、一些网络相关的硬件设备
2.1、网卡
计算机上网必备硬件,cpu靠网卡来链接外部网络的
串行转并行的设备;因为cpu和网卡都是串行的,但是网络传输的数据是并行的,需要网卡中转
数据帧封包和拆包;
网络数据的缓存和速率适配;
2.2、中继器
中继并用来放大信号
2.3、集线器
相当于中继器有中继和放大信号的作用,并且还可以组成局域网,用广播的形式工作,注意集线器只能用来组成局域网而不能用来连接外网的
2.4、交换机
包含集线器的功能,但是其工作方式比广播更高级,交换机中存在地址表,会进行学习记录,有记录的能直接发生,没有的才广播后记录下次就不用再广播形式发送了。
本质功能;学习MAC地址进行帧转发
2.5、路由器
本质功能是数据包转发和路径选择
路由器对外实现联网,对内管理子网
对内管理子网:可以在路由器中上设置子网的网段,设置有线端口的IP地址,设置dhcp功能等,因此局域网的IP是由路由器决定的。
对外实现联网;联网方式取决于外部网络,此时路由器相当于一个节点。
3、网络中的一些技术协议
3.1、DNS 域名服务
提供域名和IP地址直接转换服务的
3.2、DHCP 动态主机配置协议
局域网内的个主机IP地址是动态分配的,而动态分配需要局域网内的DHCP服务器来协助才能完成,
DHCP动态分配的优势在于;方便接入和断开,有限的IP地址得到充分利用
3.3、NAT 网络地址转换协议
IP地址分为私网IP和公网IP,在局域网内为私有IP ,不同局域网IP地址可以相同,但是在互联网连接时为公网IP此时IP地址不能重复的,因此需要NAT进行转换。
NAT的作用就是缓冲IPV4的IP地址不够的问题
3.5、NAT穿透
P2P技术就是的,
因为NAT 网络地址转换协议则把不同局域网之间进行了隔离,而NAT穿透就是直接让两个不同局域网内的主机进行通信的技术。主要是通过一个服务器(寻找种子)作为中介完成不同局域网的两台主机通信的。
4、IP地址分类
IP地址 = 网络地址 + 主机地址;
网络地址位数决定这种网络有多少个子网络。
主机地址位数决定该子网络能有多少主机
子网掩码;用来说明网络地址和主机地址各站多少位的,网络地址位置置0置1,主机地址位置置0
二、网络编程
1、socket编程接口介绍
1.1、 socket
类似于文件IO中的open传入地址属性返回文件描述符,这里是用来打开一个网络连接,成功则返回一个网络描述符,之后操作这个网络连接就通过这个网络描述符即可。
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain;表示网络域,就是网络范围,ipv4还是ipv6有相关宏设置AF_INET、AF_INET6
type;通信协议的类型,tcp,udp...也有相关的宏来设置
protocol 一般传0表示无特例,默认协议
1.2、bind
类似于文件IO中的fcntl函数给打开的文件描述符操作其相关属性,而bind就是给打开的网络文件描述符绑定自己的IP地址
注意;这个函数IP地址可兼容IPV4\IPV6两种
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd;网络文件描述符
const struct sockaddr * addr;一个指向特定协议的地址结构的指针
socklen_t addrlen;该地址结构的长度
1.3、listen
isten函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
当调用listen之后,服务器进程就可以调用accept来等待接受一个外来的请求。
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
成功返回0 ,失败返回-1并设置erron
backlog 表示监听最大的排队数,也就是一起最多可以监听多少个客户端
1. 4、connect;连接
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
成功返回0 ,失败返回-1并设置erron
const struct sockaddr *addr;表示要连接的ip地址
1.5、收发
发送接收send和write,recv和read 基本相同;只是send多个参数flag,但是一般传0,则两个一样
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
1.6、网络编程中大小端不匹配问题
一般操作系统为小端模式,高字节存放在高地址上,而在网络编程中网络字节序列(或是通信协议协议)中数据的顺序是大端模式,高字节都是放在低地址上。因此需要一些转换函数,
用来将主机short类型的字节顺序转为网络字节顺序。
htons : h(host) to n(net) s (short):
用来将网络 short 类型的字节顺序转为主机字节顺序。
ntohs : n(net) to h(host) s(short) :
用来将主机字节顺序转换为网络字节顺序,只不过转换变量的类型不同为 unsigned long
htonl : h(host) to n (net) l (long ) :
用来将网络字节顺序转换为主机字节顺序,转换的变量为 unsigned long
ntohl: n (net) to h(host) l (long) :
1.7、网络编程中ip地址的存放以及相关结构体
in_addr 、sockaddr_in 、sockaddr_in 6
都定义在/usr/include/netinet/in.h中
首先最底层的肯定还是一个存放23位进制的数字,至于我们所输入的带.的IP地址里面会有辅助函数来进行转换的。这个数字封装的变量为 typedef u32_t in_ddr_t;
然后将这个类型单独定义一个变量封装到一个结构体中;
注意;struct in_addr存储的ip地址是网路字节序即大端方式
struct in_addr {in_ddr_t s_addr;}
再然后就有了IPV4所表示的结构体
stuct sockaddr_in
{
in_port_t sin_port;//端口号
struct in_addr sin_addr;//IP地址
等一些其他参数
}
还有了IPV6的结构体stuct sockaddr_in6其中与ipv4不同的是它封装的是struct in6_addr sin_addr;//IP6地址.
还有一个抽象的结构体 struct sockaddr 标准网络地址结构体,类似于void*可以转化为
stuct sockaddr_in 或stuct sockaddr_in的
struct sockaddr
因此对struct sockaddr进行赋值,则要
sockaddr.sin_addr.s_addr = inet_addr(“192.168.1.10”);这样的层级
sockaddr格式
在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
并且注意;在使用函数时需要把socketaddr_in强制类型转换为(struct sockaddr *)。
struct addrinfo
addrinfo结构体定义如下:
typedef struct addrinfo {
int ai_flags;//指示在getaddrinfo函数中使用的选项的标志。
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;//指向链表中下一个结构的指针。此参数在链接列表的最后一个addrinfo结构中设置为NULL。
} ADDRINFOA, *PADDRINFOA;
复制代码
其中:
ai_addrlen: 指向的缓冲区的长度(以字节为单位)。
ai_canonname: 主机的规范名称。
ai_addr: 指向 sockaddr 结构的指针。每个返回的addrinfo结构中的ai_addr成员指向一个填充的套接字地址结构。
每个返回的addrinfo结构的长度(以字节为单位)在ai_addrlen成员中指定。
8、辅助性函数
ip地址转换 n表示net网络端;如何将输入的带.的ip地址转换为网络编程存储的32位数的ip地址
inet_aton、inet_addr、inet_ntoa 这些不支持IPV6
inet_ntop、inet_pton
int inet_aton ( const char *cp , struct in_addr *in ) ;
参数:
cp 指向点分格式IP地址字符串指针,"xxx.xxx.xxx.xxx"
in 它是 值-结果 类型的参数,用做提取与存放函数转换后的结果,其中 struct in_addr 类型与
struct sockaddr_in 中的sin_addr 字段类型是一致的,struct in_addr 中的 s_addr 变量存放着
16进制网络字节顺序的IP地址
返回值:执行成功返回非零数值,失败返回 0
in_addr_t inet_addr(const char *cp);
也是转换为32位的数
char * inet_ntoa( struct in_addr_in ) ;
参数:
in : 该参数存放用来转换的16进制网络顺序的IP地址结构体
返回值:
如果函数 Inet_ntoa 成功将 16进制网络顺序的 IP地址转换为字符串类型的点分IP 地址,
则将该字符串作为返回值返回,转换失败,返回 NULL
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(, const void *src,char *dst, socklen_t size);
p;表达式类型,就是带.的字符串IP地址
n网络存储32位形式的
int af表示使用ipV4还是IPV6有对应的宏表示AF_INET,AF_INET6
实践
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main()
{
struct in_addr addr;
if(inet_pton(AF_INET, "127.0.0.1", &addr.s_addr) == 1)
printf("Numeric IP: %x\n", addr.s_addr);
char str[16];
if(inet_ntop(AF_INET, &addr.s_addr, str, sizeof(str)))
printf("Presentation IP: %s\n", str);
return 0;
}
2、网络编程实践
完成一个cs架构的,客户端向服务器发送学生进行注册
服务端采用线程方式,主线程负责监听,子线程负责与连接过来的客户端通信,实现单服务器多客服端操作
//netw.h头文件
#ifndef _NETW_H_
#define _NETW_H_
#define SERADDR "192.168.91.129" // 服务器开放给我们的IP地址和端口号
#define SERPORT 9003 //设置服务器通信端口
#define BACKLOG 100 //设置服务器端一次最多能接收多大
#define CMD_REGISTER 1001 // 注册学生信息
#define CMD_CHECK 1002 // 检验学生信息
#define CMD_GETINFO 1003 // 获取学生信息
#define STAT_OK 30 // 回复ok
#define STAT_ERR 31 // 回复出错了
typedef struct commu
{
char name[20]; // 学生姓名
int age; // 学生年龄
int cmd; // 命令码
int stat; // 状态信息,用来回复
}info;
#endif
//服务器server.c文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "netw.h"
#include <pthread.h>
//使用子线程完成对客户端的通信操作
void *func(void *arg)
{
int ret = -1;
//进行读写操作
// 客户端反复给服务器发
while (1)
{
info st;
// 回合中第1步:服务器收
ret = recv(*(int *)arg, &st, sizeof(info), 0);
// 回合中第2步:服务器解析客户端数据包,然后干活,
if (st.cmd == CMD_REGISTER)
{
printf("用户要注册学生信息\n");
printf("学生姓名:%s,学生年龄:%d\n", st.name, st.age);
// 在这里服务器要进行真正的注册动作,一般是插入数据库一条信息
// 回合中第3步:回复客户端
st.stat = STAT_OK;
ret = send(*(int *)arg, &st, sizeof(info), 0);
}
if (st.cmd == CMD_CHECK)
{
}
if (st.cmd == CMD_GETINFO)
{
}
}
return 0;
}
int main()
{
int sockfd = -1;
int ret = -1, clifd = -1;
socklen_t len = 0;
//因为采用IPV4则可以直接使用struct sockaddr_in结构体
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
pthread_t th[10] = {-1};
int cnt = 0;
//1、创建socket接口
//AF_INET表示使用ipv4 SOCK_STREAM表示使用tcp连接协议
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
printf("socket error\n");
return -1;
}
printf("socket success.\n");
//2、bind绑定本地IP和端口与socket描述符
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址,注意层级
ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
printf("bind error\n");
return -1;
}
printf("bind success.\n");
//3、listen监听
ret = listen(sockfd, BACKLOG); //设置可以监听的最大个数
if (ret < 0)
{
printf("listen error\n");
return -1;
}
//服务器再用循环监听客服端连接,而与客户端通信采用开辟子线程的方式
while(1)
{
//4、accept阻塞等待连接,此时返回的fb描述符就是用于读写的文件描述符,
//之前socket返回的描述符是用于网络通信的
clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
printf("连接已经建立,client fd = %d.\n", clifd);
cnt++;
ret = pthread_create(&th[cnt], NULL, func, &clifd);
if (ret != 0)
{
printf("pthread_create error.\n");
exit(-1);
}
}
printf("等待回收子线程\n");
for(int i = 0; i < cnt; i++)
{
ret = pthread_join(th[i], NULL);
if (ret != 0)
{
printf("pthread_join error.\n");
exit(-1);
}
}
return 0;
}
//客户端 client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "netw.h"
char sendbuf[100];
char recvbuf[100];
int main()
{
int sockfd = -1, ret = -1;
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
// 第1步:socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
printf("socket error\n");
return -1;
}
printf("socketfd = %d.\n", sockfd);
// 第2步:connect链接服务器
seraddr.sin_family = AF_INET; // 设置地址族为IPv4
seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址
ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
perror("listen");
return -1;
}
printf("成功建立连接\n");
//第3步;就是进行读写操作
while (1)
{
// 回合中第1步:客户端给服务器发送信息
info st1;
printf("请输入学生姓名\n");
scanf("%s", st1.name);
printf("请输入学生年龄");
scanf("%d", &st1.age);
st1.cmd = CMD_REGISTER;
//printf("刚才输入的是:%s\n", sendbuf);
ret = send(sockfd, &st1, sizeof(info), 0);
printf("发送了1个学生信息\n");
// 回合中第2步:客户端接收服务器的回复
memset(&st1, 0, sizeof(st1));
ret = recv(sockfd, &st1, sizeof(st1), 0);//是堵塞的,与创建套接字socket的时候设置有关
// 回合中第3步:客户端解析服务器的回复,再做下一步定夺
if (st1.stat == STAT_OK)
{
printf("注册学生信息成功\n");
}
else
{
printf("注册学生信息失败\n");
}
}
return 0;
}