一、linux中的网络编程
1. 引入
网络编程也是属于进程间通信的一种方式
前面学习:管道,信号,共享内存,消息队列,信号量(共同的特点:只能用于同一台主机内部的不同进程间通信)
网络编程:既能在同一台主机内部通信,也可以在不同主机间通信
2.网络的理论概念
(1)网络分层模型
为了方便大家理解计算机网络的通信过程,人为地把计算机网络分成了七个层次
OSI七层模型,每个层次都有自己的通信协议(游戏规则)
应用层 http协议(网页) ftp协议(文件传输协议) telnet(远程登录访问)
会话层 数据的加密,解密
表示层 前面三个层的作用,提供程序员开发应用程序需要的通信协议
传输层 在计算机网络帮助我们传输数据的,常用的协议的有tcp协议,udp协议
网络层 作用:帮助我们寻找数据传输的最佳路径(路由功能),路由器中有个路由表(存放了所有连接的主机的MAC地址和ip地址,通过算法寻找最佳传输路径) 常用协议:ip协议
数据链路层 作用:开发网卡的驱动就需要用到数据链路层的协议
网络接口层(物理层) 作用:开发网卡的驱动就需要用到物理层的协议
tcp/ip模型,七层简化成了4层
应用层、传输层、网络层(ip层)、物理层
(2)ipv4和ipv6
ipv4: 32位地址 192.168.19.2(点分十进制ip),三个小数点划分成了四个部分,每个部分各占一个字节
ipv6: 解决ipv4地址不够用的问题,占128位
(3)端口号
本质是一个无符号的短整数,取值范围0–65535之间,程序员可以自己指定端口号,但是千万不要使用1024以内的端口号(很多被操作系统占用),作用是为了区分同一台主机内部不同的网络进程
同一台主机内部,端口号一定不能相同,在不同的主机上端口号可以一样
3.tcp协议相关的接口函数
tcp通信基于c/s模式(client/server)
(1) 创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
返回值:成功 返回套接字的文件描述符
失败 -1
参数:domain --- 地址协议类型
ipv4地址 --- AF_INET或者PF_INET
ipv6地址 --- AF_INET6或者PF_INET6
type --- 套接字的类型
tcp套接字(数据流套接字,流式套接字) --- SOCK_STREAM
udp套接字(数据报套接字) --- SOCK_DGRAM
protocol --- 扩展协议,一般设置0
(2) 绑定
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
int bind(int socket, const struct sockaddr_in *address, socklen_t address_len); //局限性,只能用ipv4地址
int bind(int socket, const struct sockaddr_in6 *address, socklen_t address_len); //局限性,只能用ipv6地址
返回值: 成功 0 失败 -1
参数: socket --- 创建套接字文件描述符
address --- 保存需要绑定的ip地址和端口号
struct sockaddr --- 通用地址结构体(兼容ipv4和ipv6)
{
unsigned short sa_family; //地址协议类型
char sa_data[14]; //存放ip和端口号
}
struct sockaddr_in ipv4地址结构体
{
short sin_family; //地址协议类型AF_INET或者PF_INET
short sin_port; //存放你要绑定的端口号
struct in_addr sin_addr; //存放你要绑定的ip地址 4字节
unsigned char sin_zero[8];//打酱油的,为了跟struct sockaddr大小保持一致
}
struct sockaddr_in6 ipv6地址结构体
struct in_addr
{
unsigned int s_addr; //最终用来存放ip地址
}
address_len --- 地址结构体的大小 sizeof
tcp服务器端是一定要绑定: 如果不绑定,客户端connect的时候不知道端口号写多少
tcp客户端可以绑定也可以不绑定: 绑定了,相当于程序员自己指定了一个固定的端口号
不绑定,操作系统会自动分配一个端口号(每次运行分配得都不一样,不固定)
linux提供了一个宏定义INADDR_ANY(只能在bind函数中使用)
作用:帮助我们绑定本地主机上的任意一个ip地址
例:server.sin_addr.s_addr =htonl(INADDR_ANY);
回顾:结构体大小规则
64位ubuntu
找到结构体中最大的类型(数组不要理会看数据类型)
最大类型>=8字节 整个结构体就按照8字节对齐
最大类型<8字节 整个结构体就按照最大类型的大小对齐
32位ubuntu
找到结构体中最大的类型(数组不要理会看数据类型)
最大类型>=4字节 整个结构体就按照4字节对齐
最大类型<4字节 整个结构体就按照最大类型的大小对齐
(3) ip和端口号的转换
大小端:
ubuntu系统(主机字节序):小端序存储数据
计算机网络中(网络字节序):数据采用大端序存放
第一组:小端序ip端口号 -> 大端序ip端口号
小端序存储的ip-> 大端序存储的ip(把主机字节序ip转换成网络字节序ip)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
返回值:成功 --- 返回转换得到大端序二进制ip
失败 --- -1
参数:cp --- 你需要转换的点分十进制小端序ip
主机字节序端口号-> 转换成网络字节序端口号
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort)
返回值:成功 --- 返回转换得到的大端序端口号
失败 --- -1
参数:hostshort --- 小端序端口号
第二组:大端序ip端口号 -> 小端序ip端口号
大端序存储的ip -> 小端序存储的ip
char *inet_ntoa(struct in_addr in);
返回值:转换得到的字符串ip(小端序)
失败 --- -1
参数:in --- 大端序ip
大端序存储的端口号 -> 小端序存储的端口号
uint16_t ntohs(uint16_t netshort);
返回值:返回转换得到的小端序端口号
失败 --- -1
参数:netshort --- 大端序端口号
(4)连接
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
参数: socket --- 创建套接字文件描述符
address --- 存放对方的ip和端口号
address_len --- 地址结构体的大小
(5)监听
#include <sys/socket.h>
int listen(int socket, int backlog);
参数:socket --- 创建套接字文件描述符
backlog --- 最多允许多少个客户端同时连接服务器
比如: listen(tcpsock,5);
(6)愿意接收客户端的连接请求
#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
返回值:成功 --- 返回一个新的套接字(用来跟客户端通信)
失败 --- -1
参数:socket --- 创建套接字文件描述符
address --- 存放客户端(对方)的ip和端口号
服务器会自动把目前连接成功的那个客户端ip和端口号存放到这个变量中
address_len --- 地址大小,要求是指针
总结accept的特点:
①accept会阻塞服务器,直到有客户端连接服务器才会解除阻塞
②每连接一个客户端,服务器accept都会返回一个对应的新套接字
(7)收发信息
①read和write
如果通信双方任意一方断开连接(退出了),会导致read不再阻塞,并且read返回值为0
返回值:>0 --- 接收到网络中数据字节数
==0 --- 说明对方断开连接了(实际开发的时候可以去判断read的返回值,为0退出你的程序)
-1 --- 说明read调用失败
使用情况:
第一种:当成文件IO的函数使用,read读取某个文件里面的内容,如果文件读取完毕,read的返回值就是0
第二种:在网络编程中使用,read返回0表示对方断开连接
②send和recv
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int socket, const void *buffer, size_t length, int flags); //相当于write
参数:前面三个跟write一样的,最后一个参数设置为0
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int socket, void *buffer, size_t length, int flags); //相当于read
返回值:>0 --- 接收到网络中数据字节数
==0 --- 说明对方断开连接了(实际开发的时候可以去判断recv的返回值,为0退出你的程序)
-1 --- 说明recv调用失败
4. 运行代码出现的问题
问题一: 代码刚退出,立马执行会出现绑定失败
绑定失败!: Address already in use
原因: linux系统中,程序退出了,绑定的端口号并不会立马释放掉,会延迟大概半分钟左右才释放
解决方法一: 更换绑定的端口号,利用主函数传参
解决方法二: linux提供了设置套接字属性的函数(使用频率最高)
#include <sys/socket.h>
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
参数:socket --- 创建套接字文件描述符
level --- SOL_SOCKET
option_name --- SO_REUSEADDR //取消端口号绑定时候重复使用的限制
option_value --- 指针,指向存放选项待设置的新值的缓冲区
option_len --- option_value缓冲区长度
例:
int on=1;
setsockopt(tcpsocket,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));