Linux在网络编程中的操作主要是针对是传输层开始,利用传输层的协议去传输数据
传输层两大协议:
TCP协议:TCP全称 Transmition Control Protocol,即:传输控制协议。是面向连接的协议。通常,TCP 通信还会被冠以 可靠传输协议 的头衔。但请注意,这里的可靠并非指发出去的数据对方一定能收到(这是不可能的),而仅指TCP能使发送方可靠地知道对方是否收到了数据。
通信特点:1,面向连接的通信;2,可靠的传输;3,流操作特性;4,星状网络;
如何实现TCP的通信呢?

TCP的代码离不开两个概念:
服务器与客户端;
具体的通信过程:
从起点 CLOSED 到 ESTABLISHED 的过程,就是TCP建立连接的过程,在这个过程中:
客户端
通过 connect() 向服务端发送 SYN 同步请求数据,然后收到服务器的 SYN/ACK包之后 并回送 ACK 后处于 ESTABLISHED 状态
服务端
通过函数 listen() 将TCP套接字设置为 LISTEN 状态,在收到客户端的 SYN 请求并回送 SYN/ACK 后处于 SYN_RCVD 状态,最后收到客户端的 ACK 后与客户端一并处于 ESTABLISHED 状态,至此,TCP建立完毕,这个过程就是著名的 三次握手 的详细过程,如下图所示:

TCP协议三次握手
在断开连接的时候进行四次握手:
与 三次握手 类似,在TCP断开连接的时候,客户端和服务器也会相互发送断开请求和确认,而又因为允许一方断开读端或写端而保持另一端的通畅,因此两端的关闭请求可以不必同时发生,因此中间的 FIN 和 ACK 就不像连接时那样经常合并在一起,而是常常分开传输,因此断开连接的过程通常体现为四次挥手,如下图所示:

TCP协议四次挥手
服务器的代码如何编写:
网络初始化步骤:
1.socket:用来申请网络接口的
2.bind:绑定函数,用来将申请网络接口与指定的IP及端口绑定在一起,以后一旦这个指定的IP跟端口一定有数据,这个网络接口便会收到;
3.listen:开启网络监听,相当于将你的这个网络接口公开出来给人连接
网络处理步骤:
1.accept:处理客户端连接,每次调用这个函数都可以处理一个客户端连接,所以如果你要处理多个客户端的连接,便要一直调用这个函数
2.recv/send:用来收发TCP网络数据
3.shutdown:关闭网络端口的读写功能
4.close:关闭文件描述符(其中socket返回的是网络接口的文件描述符,accept返回的是与客户端通信的文件描述符)
客户端的代码如何编写:
1.socket:用来申请网络接口
2.bind:可用可不用,如果用的话便是客户端程序指定一个固定的端口,不用的话,以后发送数据的时候,系统随机的给你分配网络接口(IP及端口)
3.connect:连接服务器(当服务器listen之后便可以连接),连接之后便可以与服务器进行通信
4.recv/send:用来收发TCP网络数据
5.shutdown:关闭网络端口的读写功能
6.close:关闭文件描述符(其中socket返回的是网络接口的文件描述符,accept返回的是与客户端通信的文件描述符)
函数分析
socket()
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数功能:
申请通信套接字(接口)
参数:
Domain:申明通信协议:
AF_UNIX, AF_LOCAL unix本地域通信 unix(7)
AF_INET IPV4的协议 ip(7)
AF_INET6 IPV6的协议 ipv6(7)
根据上面的协议,我们选择了IPV4的协议,便要看man 7 ip手册
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
tcp_socket = socket(AF_INET, SOCK_STREAM, 0);//申请TCP接口
udp_socket = socket(AF_INET, SOCK_DGRAM, 0);//申请UDP接口
raw_socket = socket(AF_INET, SOCK_RAW, protocol);//申请原始套接字
Type:子级协议
SOCK_STREAM:流操作(TCP)
SOCK_DGRAM:报文传输(UDP)
Protocol:拓展说明
默认写0
函数返回值:
成功返回套接字的文件描述符,失败返回-1
bind()
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
函数功能:
将套接字与指定的IP及端口绑定在一起
函数参数:
Sockfd:套接字的文件描述符
Addr:你要绑定的主体,这里的主体类型是struct sockaddr *,他是一个通用套接字结构(原因是因为socket所申请接口可能有很多种协议,为了实现bind函数可以绑定多个不同类型的信息,所定义出了通用结构体),不同的协议手册当中建议你去看不同协议的结构体,所以我们根据指引,去看了man 7 ip,里面有IPV4专用的结构体:
struct sockaddr_in {
sa_family_t sin_family; /* 协议族: AF_INET */
in_port_t sin_port; /* 网络字节序 */
struct in_addr sin_addr; /* 网络地址结构体 */
};
/* 网络地址结构体 */
struct in_addr {
uint32_t s_addr; /* 网络字节序 */
};
sin_family:代表用什么协议,这里规定用AF_INET,也就是IPV4的协议
sin_port:代表端口号,其中他要求用网络字节序的数据,根据man手册指引,让我们用htons函数转化
sin_addr:代表IP地址信息,他是一个结构体struct in_addr类型,里面只有一个元素s_addr,是一个32位的网络字节序的数据,所以我们如果要填充IP地址,就必须转化成为32位的网络字节序的数据,所以有以下两种操作:
1.根据手册说明,你可以使用inet_addr系列函数转化字符串的IP地址
2.利用INADDR_ANY(所有本地ip)转化成为网络字节序的数据填入,所以我们可以用htonl进行转化
注:传入的时候,是调用struct sockaddr_in*结构体数据,为了消除警告,进行强制转化成为bind函数要求的struct sockaddr*结构体数据
Addrlen:传入的结构体大小
函数返回值:
成功返回0,失败返回-1
listen()
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函数功能:
开启套接字的网络监听,让客户端可以开始连接进来了
参数:
Sockfd:套接字的文件描述符
Backlog:最大可以缓冲还没有处理的客户端数量(也就是等待的队列)
返回值:
成功返回0,失败返回-1
accept()
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数功能:
处理客户端的一个连接,返回客户端的文件描述符,以后服务器与客户端收发数据便是引用该文件描述符进行读写
参数:
Sockfd:套接字的文件描述符
Addr:用来存放客户端的地址信息,如果你不想要,可以直接填入NULL
(该类型的数据请参照bind函数的说明)
Addrlen:代表addr结构体的长度,该数值需要先填充号addr结构体的长度,客户端连接成功之后返回客户端的结构体的长度(也就是一值两用),如果不需要,可以直接跟addr一起填入NULL
返回值:
成功返回处理的客户端的文件描述符,失败返回-1
获取到的客户端的地址信息如何解析出来呢?
ntohs:网络转主机字节序
inet_ntoa:网络字节序的IP地址,转化为本机的字符串IP地址
recv()/send()
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数功能:
Recv:用来接受指定文件描述符的网络数据(通过客户端的文件描述符接受数据)
Send:通过指定文件描述符发送网络数据(通过客户端的文件描述符发送数据)
参数:
(前面三个参数请参照read/write,代表发送/接受哪个文件(客户端),发送的数据在哪里/接受的数据存放在哪里,发送及接受的长度)
Flags:默认写0
函数返回值:
成功则返回接受或者发送了多少个字节,失败返回-1,如果recv返回0则代表对方断开连接
send的flags参数:
MSG_DONTROUTE
不要使用网关发送数据包,只发送到主机
在用直连线直接连接的网络上。这通常仅由
诊断或路由程序。这仅针对原型定义
这条路线的col 家庭;数据包套接字没有。
MSG_DONTWAIT(从 Linux 2.2 开始)
启用非阻塞操作;如果操作会阻塞,EA-
返回GAIN 或 EWOULDBLOCK。这提供了类似的行为
设置O_NONBLOCK 标志(通过 fcntl(2) F_SETFL 操作
化),但不同之处在于MSG_DONTWAIT 是一个每次调用选项,
而O_NONBLOCK 是打开文件描述的设置
(参见open(2)),这将影响调用中的所有线程
进程以及其他保存文件描述的进程
Tors 指的是相同的打开文件描述。
MSG_MORE(自 Linux 2.4.4 起)
调用者有更多数据要发送。此标志与TCP 一起使用
套接字以获得与TCP_CORK 套接字选项相同的效果
(参见tcp(7)),不同的是这个标志可以设置在
每次通话。
从Linux 2.6 开始,UDP 套接字也支持此标志,
并通知内核打包调用中发送的所有数据
将此标志设置为发送的单个数据报
仅当执行未指定此标志的调用时。
(另请参见udp(7) 中描述的 UDP_CORK 套接字选项。)
MSG_NOSIGNAL(自 Linux 2.2 起)
如果对等点是面向流的,则不要生成SIGPIPE 信号
套接字已关闭连接。EPIPE 错误仍然存在
转身。这提供了与使用sigaction(2) 类似的行为
忽略SIGPIPE,但是,虽然 MSG_NOSIGNAL 是每次调用的功能,
忽略SIGPIPE 设置影响所有进程的进程属性
进程中的线程。
MSG_OOB
在支持此概念的套接字上发送带外数据
(例如,SOCK_STREAM 类型);底层协议也必须
支持带外数据。
recv函数flags参数内容:
MSG_DONTWAIT(从 Linux 2.2 开始)
启用非阻塞操作;如果操作会阻塞,则
调用失败并出现EAGAIN 或 EWOULDBLOCK 错误。这提供了
与设置O_NONBLOCK 标志类似的行为(通过 fc-
ntl(2) F_SETFL 操作),但不同之处在于 MSG_DONTWAIT 是
per-call 选项,而 O_NONBLOCK 是打开时的设置
文件描述(见open(2)),这将影响所有线程
调用进程以及其他持有的进程
文件描述符引用相同的打开文件描述。
MSG_OOB
此标志请求接收不会被
在正常数据流中接收。一些协议需要经验
在普通数据队列的头部编辑数据,因此这
flag 不能与此类协议一起使用。
MSG_PEEK
该标志使接收操作从
接收队列的开始,而不从中删除该数据
队列。因此,后续的接收调用将返回相同的
数据。
MSG_TRUNC(自 Linux 2.2 起)
对于原始 (AF_PACKET)、Internet 数据报(自 Linux
2.4.27/2.6.8)、netlink(自 Linux 2.6.22 起)和 UNIX 数据报
(Linux 3.4 起)sockets:返回数据包的实际长度
或数据报,即使它比传递的缓冲区长。
要与 Internet 流套接字一起使用,请参阅 tcp(7)
MSG_WAITALL(自 Linux 2.2 起)
此标志请求操作阻塞,直到完全重新
任务得到满足。但是,调用可能仍然返回较少
如果捕获到信号、错误或中断,则数据比请求的多
nect 发生,或下一个要接收的数据是不同的
类型比返回的。该标志对数据报没有影响
插座。
shutdown()
#include <sys/socket.h>
int shutdown(int sockfd, int how);
函数功能:
用来关闭文件描述符的某个功能
参数:
Sockfd:套接字的文件描述符
How:要关闭的功能:
SHUT_RD,SHUT_WR, SHUT_RDWR
返回值:
成功返回0,失败返回-1
connect()
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
函数功能:
客户端用来连接服务器的专用函数,一旦调用成功便会让sockfd代表服务器
参数:
Sockfd:套接字的文件描述符
Addr:要连接的服务器的地址信息
Addrlen:地址信息的整个大小
(详情请看bind函数说明)
返回值:
成功返回0,失败返回-1
UDP的通信特性:
UDP全称 User Datagram Protocol,即:用户数据报协议。是面向无连接的协议。通常,UDP 通信还会被冠以不可靠的头衔。这里的不可靠指的是:无法可靠地得知对方是否收到数据。
UDP有如下特征:
1.无连接:通信双方不需要事先连接
2.无确认:收到数据不给对方发回执确认
3.不保证有序、丢失不重发
4.他也有差错处理(错了就丢失,他不会将错误的数据反馈给上层应用)
5.采用帧同步的数据报通信方式(即通信双方每次的收发数据量相等)
6.UDP是没有服务器与客户端的概念,它支持比较特殊的两种通信方式:广播与组播
操作流程:
1.socket:申请套接字
2.bind:可绑定或者不绑定,决定是否要给其一个固定的端口
3.recvfrom/sendto:进行收发
4.close:关闭通信
函数分析
recvfrom()/send()
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
函数功能:
Sendto:通过套接字给指定的目标地址发送数据,他需要指明对方的地址信息
Recvfrom:接受套接字的数据,将对方的地址信息及数据存放起来
参数:
(前面三个参数请参照read/write,代表发送/接受哪个文件(套接字),发送的数据在哪里/接受的数据存放在哪里,发送及接受的长度)
Flags:操作标志,默认写0
(其他参数请参照recv/send函数)
dest_addr,addrlen:指定发送给谁,需要将对方的地址信息填充进来
(该结构请参照bind函数,addrlen代表地址信息的大小)
src_addr,addrlen:用来存放发送数据过来的那个人的具体地址信息
(该结构请参照accept函数,如不需要也可填入NULL,addrlen一值两用,请参照accept)
函数返回值:
成功则返回接受或者发送了多少个字节,失败则返回-1
setsockopt()
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
函数功能:
设置套接字属性
参数分析:
sockfd:套接字的文件描述符
level:设置套接字属性的层次
这个层次在IPV4当中有两个值可以选择
man 7 socket 跟你说明socket通用设置
man 7 ip 跟你说明IPV4协议的设置
根据上面所述,我们的level有两个值可以选择:
SOL_SOCKET 这个是socket通用设置
IPPROTO_IP 这个是IPV4的的通用设置(我们一般用这个)
Optname:操作的名字(见下表的属性名称)
Optval:代表操作的数值(见下表的数据格式,定义然后取地址)
Optlen:数值的大小为多大(sizeof( )上述定义的数据)
返回值:
成功返回0,失败返回-1

(组播与广播的概念都是针对UDP通信的)
注:
1.不同的属性的数据格式是不同的:
2.bool格式表示开关型属性,比如是否地址复用、是否使能广播等。
3.int格式表示数值型属性,比如缓冲区大小。
4.还有一些属性拥有特定的数据格式,比如超时控制格式是struct timeval,组播struct ip_mreq
带外/加急数据(说明:man 2 fcntl)(TCP通信)
带外数据也被称之为加急数据包,指的是在发送及接受数据的使用给recv/send函数的flags参数指明MSG_OOB,他的特点是当带外数据发送过去给对方,对方会触发SIGURG信号,我们只要捕捉这个信号去读取带外数据即可(带外数据包跟普通数据包一样,都是会先存放在接受缓冲区当中,当我们调用recv,flags设置位MSG_OOB的时候可以优先读取出来)
IO模型:
(一个服务端网络程序需要同时处理多个套接字的处理策略)
非阻塞轮询:
long state = fcntl(sockfd, F_GETFL);
state |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, state);
通过fcntl()设置为不阻塞的套接字之后, 在while循环读取期间,系统CPU单核利用率达100%,程序一直在做无用的循环,白白浪费系统资源,因此轮询的IO方式一般是不被采用的。
多任务并发模型(TCP群聊)
每个线程处理一个已连接套接字
线程一般要处于分离状态,避免产生僵尸
异步信号驱动(UDP)
/*服务端不管套接字收到何种数据之前,内核一律触发 SIGIO*/
// 1. 设置捕捉信号 SIGIO,其处理函数来接收数据,主函数要一直运行
signal(SIGIO, f);
// 2. 设置信号的属主:指定信号的接受者的 PID
fcntl(sockfd, F_SETOWN, PID);
// 3. 将套接字设置为异步工作模式:
long state = fcntl(sockfd, F_GETFL);
state |= O_ASYNC;
fcntl(sockfd, F_SETFL, state);
多路复用IO模型
select()
#include <sys/select.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
函数功能:
多路监听函数,可以一次性监控多个文件描述符的状态,默认是阻塞的
参数:
nfds:要监控的文件描述符当中的最大值再+1
readfds:要监控的读文件描述符集合
writefds:要监控的写文件描述符集合
exceptfds:要监控其是否异常的文件描述符集合
timeout:超时等待的时间设置
(上面的参数如果不需要设置,则直接赋值为NULL即可)

FD_CLR():用来清空指定的文件描述符在文件描述符集合中
FD_ISSET():用来判断文件描述符是否在指定的集合中
FD_SET():将某个描述符设置到集合中
FD_ZERO():将描述符集合清空
(参数见上图)
poll()
#include <poll.h>
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout);
(函数功能与参数可参考上述select();函数)
函数工作细节:
他是一个大门,可以先阻塞在这里监控所有参数当中传入的文件描述符集合里面的文件描述符(也就是一次性监听一群文件描述符(可以用链表结构存放)),当有人来数据(不管是谁的)便不在阻塞,其中他会将read(fds...),write(fds...),except(fds...)的集合中,只保留唤醒它的文件描述符,从而让你在被唤醒后可以通过判断哪个文件描述符还在集合当中来确定是谁来了数据
网卡信息获取:
通过ioctl函数可以获取网卡及套接字信息:
(详情请参照:自己的工具链arm-none-linux-gnueabi/libc/usr/include/bits/ioctls.h里面有关于获取网卡信息的宏的说明)
/* 某一个网卡信息 */
struct ifreq
{
union
{
char ifrn_name[IFNAMSIZ]; /* 某个网口的名称,比如 "ens33" */
} ifr_ifrn;
union
{
struct sockaddr ifru_addr; // IP地址
struct sockaddr ifru_dstaddr; // 目标IP地址
struct sockaddr ifru_broadaddr;// 广播地址
struct sockaddr ifru_netmask; // 子网掩码
struct sockaddr ifru_hwaddr; // 硬件MAC地址
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
__caddr_t ifru_data;
}ifr_ifru;
};
注意到,某网卡的相关信息被放在一个联合体中,换句话说不能一次性获取这些信息,而是要用如下宏来分别获取这些信息:
序号 | 宏 | ioctl参数 | 描述 |
1 | SIOCGIFCONF | struct ifconf | 获取所有活跃网卡的信息 |
2 | SIOCGIFADDR | struct ifreq | 获取指定网卡的IP地址信息 |
3 | SIOCGIFBRDADDR | struct ifreq | 获取指定网卡的广播地址信息 |
4 | SIOCGIFHWADDR | struct ifreq | 获取指定网卡的硬件MAC地址信息 |
网卡信息获取的操作步骤:
struct ifreq info;//声明网卡信息结构体
strcpy(info.ifr_ifrn.ifrn_name, "ens36");//将网卡名字放入结构体当中
ioctl(skt_fd, SIOCGIFADDR, &info);//获取指定网卡信息(宏定义见上表)
printf("ipadds=%s\n",
inet_ntoa(((struct sockaddr_in*)&info.ifr_ifru.ifru_addr)->sin_addr));
//上面的分析方式是将他内存分析方式转化位IPV4结构体的分析方式
printf("port=%hu\n", ntohs(((struct sockaddr_in *)&info.ifr_ifru.ifru_addr)->sin_port));
getsockname()
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数功能:
通过域名获取目标的地址信息:
参数
sockfd - 套接字文件描述符
addr - 存储套接字所绑定的地址信息,这个是通用结构体,你需要传入IPV4的结构体获取
addrlen - 地址结构体长度
返回值
成功返回 0
失败返回-1
gethostbyname()
#include <netdb.h>
extern int h_errno;
struct hostent *gethostbyname(const char *name);
函数功能:
获取指定域名的详细信息
参数:
Name:传入的域名
返回值:
成功返回一个结构体指针,失败返回NULL
struct hostent {
char *h_name; /* 主机名 */
char **h_aliases; /* 列表别名 */
int h_addrtype; /* 主机地址类型 */
int h_length; /* 地址长度 */
char **h_addr_list; /* 地址列表 */
}
(其中对于我们最重要的数据是h_addr_list,他是一个数组,数组的每个元素都放着主机的一个地址信息(服务器有可能不止一个IP),最后一个元素是以NULL作为结尾的)
由于地址列表是网络字节序的,我们需要进行转化:
for(i=0; hostinfo->h_addr_list[i]!=NULL;i++)//这个for循环就是循环获取出列表中的元素
{
printf("ip=%s\n", inet_ntoa(*(struct in_addr*)((hostinfo->h_addr_list)[i])));
//上面做了一个转化,先将数据转化为主机识别的标准的IP地址的网络字节序类型
//再通过inet_ntoa转化为我们现在的字符串格式
}
应用层:HTTP协议和HTTPS协议:
请求报文:(格式拼接:用到sprintf()拼接字符串)

响应报文:(定位字符串指针:strstr()定位指定字符串位置指针)



编程步骤:
HTTP:
从域名gethostbyname();得到服务器ip -> socket();得到服务器fd -> connect();连接到该服务器 -> 请求报文send();发到该服务器 -> 服务器响应recv();得到响应报文 -> close()关闭服务器fd(断开连接)
HTTPS:(HTTP+SSL)
使用openSSL:
从域名gethostbyname();得到服务器ip -> socket();得到服务器fd -> connect();连接到该服务器 -> 初始化SSL -> 请求报文SSL_write();发到该服务器-> 服务器响应SSL_read();得到响应报文 -> 销毁SSL加密 -> close()关闭服务器fd(断开连接)
https接口说明:
https://blog.youkuaiyun.com/zhangbaoqiang1/article/details/81628859