Linux 练习十二 (Linux网络编程socket + 源码练习)


使用环境:Ubuntu18.04
使用工具:VMWare workstations ,xshell

  作者在学习Linux的过程中对常用的命令进行记录,通过思维导图的方式梳理知识点,并且通过xshell连接vmware中ubuntu虚拟机进行操作,并将练习的截图注解,每句话对应相应的命令,读者可以无障碍跟练。
  本次练习的重点在于Linux的网络编程。
作者在编写这部分内容时,正在准备面试,所以文章中理论部分简化了很多,重点在于socket和epoll的时间编程,后期有时间必定补上网络的理论知识。

1 计算机网络基础知识

1.1 OSI参考模型和TCP/IP参考模型

在这里插入图片描述

  • tcp/ip 模型 4 层:
    应用层 {http 超文本传输协议 ftp 文件传输协议 telnet 远程登录 ssh 安全外壳协议 stmp 简单邮件发送pop3 收邮件}
    传输层 {tcp 传输控制协议,udp 用户数据包协议}
    网络层 {ip 网际互联协议 icmp 网络控制消息协议 igmp 网络组管理协议}
    网络接口层 {arp 地址转换协议,rarp 反向地址转换协议,mpls 多协议标签交换}
    TCP 协议:传输控制协议 面向连接的协议 能保证传输安全可靠 速度慢(有 3 次握手)
    UDP 协议:用户数据包协议 非面向连接 速度快 不可靠
    通常是 ip 地址后面跟上端口号:ip 用来定位主机 port 区别应用(进程)
    http 的端口号 80 ssh–>22 telnet–>23 ftp–>21 用户自己定义的通常要大于 1024

  • TCP/IP 协议族的每一层的作用:
    网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。
    网络层:负责将数据帧封装成 IP 数据报,并运行必要的路由算法。
    传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。
    应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。
    在这里插入图片描述

  • TCP/IP 协议族的每一层协议的相关注解:

·ARP:(地址转换协议)用于获得同一物理网络中的硬件主机地址。是设备通过自己知道的 IP 地址来获得自己不知道的物理地址的协议。
·RARP:反向地址转换协议(RARP:Reverse Address Resolution Protocol)RARP允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。网络管理员在局域网网关路由器里创建一个表以映射物理地址(MAC)和与其对应的 IP 地址。当设置一台新的机器时,其 RARP 客户机程序需要向路由器上的 RARP 服务器请求相应的 IP 地址。假设在路由表中已经设置了一个记录,RARP 服务器将会返回 IP 地址给机器,此机器就会存储起来以便日后使用。 RARP 可以使用于以太网、光纤分布式数据接口及令牌环 LAN ·IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。
·ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。
·IGMP:(网络组管理协议)被 IP 主机用来向本地多路广播路由器报告主机组成员的协议。主机与本地路由器之间使用 Internet 组管理协议(IGMP,Internet Group Management Protocol)来进行组播组成员信息的交互。
·TCP:(传输控制协议)为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。
·UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。

1.2 TCP 协议

1.2.1 概述

  TCP 是 TCP/IP 体系中面向连接的运输层协议,它提供全双工和可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。
  首先,TCP 要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。
  其次,TCP 采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP 规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。
  最后,采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP 采用可变长的滑动窗口,使得发送端与接收端可根据自己的 CPU 和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。

1.2.2 建立TCP连接——三次握手

  在利用 TCP 实现源主机和目的主机通信时,目的主机必须同意,否则 TCP 连接无法建立。为了确保 TCP 连接的成功建立,TCP 采用了一种称为三次握手的方式,三次握手方式使得“序号/确认号”系统能够正常工作,从而使它们的序号达成同步。如果三次握手成功,则连接建立成功,可以开始传送数据信息。
三次握手的过程是:
1)源主机 A 的 TCP 向主机 B 发送连接请求报文段,其首部中的 SYN(同步)标志位应置为 1,表示想跟目标主机 B 建立连接,进行通信,并发送一个同步序列号 X(例:SEQ=100)进行同步,表明在后面传送数据时的第一个数据字节的序号为 X+1(即 101)。
2)目标主机 B 的 TCP 收到连接请求报文段后,如同意,则发回确认。再确认报中应将 ACK 位和SYN 位置为 1.确认号为 X+1,同时也为自己选择一个序号 Y。
3)源主机 A 的 TCP 收到目标主机 B 的确认后要想目标主机 B 给出确认。其 ACK 置为 1,确认号为 Y+1,而自己的序号为 X+1。TCP 的标准规定,SYN 置 1 的报文段要消耗掉一个序号。
在这里插入图片描述
  运行客户进程的源主机 A 的 TCP 通知上层应用进程,连接已经建立。当源主机 A 向目标主机 B 发送第一个数据报文段时,其序号仍为 X+1,因为前一个确认报文段并不消耗序号。
  当运行服务进程的目标主机 B 的 TCP 收到源主机 A 的确认后,也通知其上层应用进程,连接已经建立。至此建立了一个全双工的连接。
  三次握手:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到
响应的应用程序。
TCP 的三次握手最主要是防止已过期的连接再次传到被连接的主机。

1.2.3 断开TCP连接——四次挥手

由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向的连接。收到一个 FIN 只意味着这一方向上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP 客户端发送一个 FIN,用来关闭客户到服务器的数据传送。
(2) 服务器收到这个 FIN,它发回一个 ACK,确认序号为收到的序号加 1。和 SYN 一样,一个FIN 将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个 FIN 给客户端。
(4) 客户端发回 ACK 报文确认,并将确认序号设置为收到序号加 1
在这里插入图片描述
特殊情况: 有时候断开连接的发起方可以主动断开连接,那么四次挥手会出现三次握手的情况,即最后一次不会回应。如何理解这种模式呢?用下面的对话来记忆。
在这里插入图片描述在这里插入图片描述

2 socket概念

  Linux 中的网络编程是通过 socket 接口来进行的。socket 是一种特殊的 I/O 接口,它也是一种文件描述符。它是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
  每一个 socket 都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket 也有一个类似于打开文件的函数调用,该函数返回一个整型的 socket 描述符,随后的连接建立、数据传输等操作都是通过 socket来实现的;

2.1 socket类型

(1)流式 socket(SOCK_STREAM) ——>用于 TCP 通信
流式套接字提供可靠的、面向连接的通信流;它使用 TCP 协议,从而保证了数据传输的正确性和顺序性。
(2)数据报 socket(SOCK_DGRAM) ——>用于 UDP 通信
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议 UDP。
(3)原始 socket(SOCK_RAW) ——>用于新的网络协议实现的测试等
原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

2.2 socket信息数据类型

#include<netinet/in.h>
struct sockaddr
{
	unsigned short sa_family; /*地址族*/
	char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/
};
struct sockaddr_in
{
	short int sin_family; /*地址族,AF_INET(ipv4)/AF_INET6(ipv6)*/
	unsigned short int sin_port; /*端口号*/
	struct in_addr sin_addr; /*IP 地址*/
	unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};
typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};

2.3 数据存储方式转换

  计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式)。内存的低地址存储数据的低字节,高地址存储数据的高字节的方式叫小端模式。内存的高地址存储数据的低字节,低地址存储数据高字节的方式称为大端模式。

  • eg:对于内存中存放的数 0x12345678 来说
    如果是采用大端模式存放的,则其真实的数是:0x12345678
    如果是采用小端模式存放的,则其真实的数是:0x78563412

  端口号和 IP 地址都是以网络字节序存储的,不是主机字节序,网络字节序都是大端模式。要把主机字节序和网络字节序相互对应起来,需要对这两个字节存储优先顺序进行相互转化。这里用到四个函数:htons(),ntohs(), htonl()ntohl().这四个地址分别实现网络字节序和主机字节序的转化,这里的 h代表 host,n 代表 network,s 代表 short,l 代表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用l 来代表。

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);	//将一个无符号短整型的数据转换为网络字节序,大端模式	
uint32_t htonl(uint32_t hostlong);	//将一个无符号长整型的数据转换为网络字节序,大端模式	
uint16_t ntohs(uint16_t netshort);	//将一个16位数由网络字节顺序转换为主机字节顺序,小端模式
uint32_t ntohl(uint32_t netlong);	//将一个32位数由网络字节顺序转换为主机字节顺序,小端模式

示例 htons.c

#include<stdio.h>
#include <arpa/inet.h>

int main()
{
    uint16_t port=0x1234,nport;
    nport=htons(port);//把主机字节序转为网络字节序
    printf("nport=%x\n",nport);	//打印3412
    return 0;
}

示例 ntons.c

#include<stdio.h>
#include <arpa/inet.h>

int main()
{
    uint16_t port=0x1234,nport;
    nport=htons(port);//把主机字节序转为网络字节序
    printf("nport=%x\n",nport);	//打印3412
    //把网络字节序转为主机字节序
    port=0;
    port=ntohs(nport);
    printf("port=%x\n",port);	//打印1234
    return 0;
}

2.4 地址格式的转换

  通常用户在表达地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制 Ipv6 地址),而在通常使用的 socket 编程中使用的则是 32 位的网络字节序的二进制值,这就需要将这两个数值进行转换。这里在 Ipv4 中用到的函数有 inet_aton()inet_addr()inet_ntoa(),而 IPV4 和 Ipv6 兼容的函数有 inet_pton()和 inet_ntop()。

  • IPV4函数原型:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *straddr, struct in_addr *addrptr);
char *inet_ntoa(struct in_addr inaddr);
in_addr_t inet_addr(const char *straddr); // in_addr_t 就是 unsigned long int ,代表 s_addr

函数 inet_aton(const char *straddr, struct in_addr *addrptr); 将点分十进制的IP转换为网络字节序的32位二进制数。成功返回1,失败返回0。
参数straddr:存放输入的点分十进制IP地址
参数affrptr:传出参数,转换后的32位网络地址
函数 inet_ntoa():将网络字节序的 32 位二进制数值转换为点分十进制的 IP 地址。
函数 inet_addr():功能与 inet_aton 相同,但是结果传递的方式不同。inet_addr()若成功则返回32 位二进制的网络字节序地址。

  • IPV6和IPV4的函数原型,两者都可以转化
#include <arpa/inet.h>
int inet_pton(int family, const char *src, void *dst);
const char *inet_ntop(int family, const void *src, char *dst, socklen_t len)

函数 inet_pton 跟 inet_aton 实现的功能类似,只多了一个family参数,如果指定为AF_INET,表示是IPV4,如果执行AF_INET6,表示时IPV6协议。
函数 inet_ntop 跟 inet_ntoa 类似,其中 len 表示表示转换之后的长度(字符串的长度)。

示例 2-4-1:

#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

int main()
{
	char ip[] = "192.168.0.101";
	struct in_addr myaddr;
	int iRet = inet_aton(ip,&myaddr);	//地址转2进制,16进制打印出来
	printf("%x\n",myaddr.s_addr);
	printf("%x\n",inet_addr(ip));		//地址转2进制,16进制作为返回值
	
	iRet = inet_pton(AF_INET,ip,&myaddr);//地址转2进制,指定是IPV4
	printf("%x\n",myaddr.s_addr);

	myaddr.s_addr = 0xac100ac4;
	printf("%s\n",inet_ntoa(myaddr));	//将32位二进制地址转为十进制

	inet_ntop(AF_INET,&myaddr,ip,16);	//将32位二进制地址转为十进制,指定IPV4
	puts(ip);
	return 0;
}

2.5 域名转化为地址

举个例子,所谓的网址就是域名,比如www.baidu.com,域名和地址的转换主要用到了DNS协议,这里不做解释,读者有兴趣自己去了解一下。我们主要介绍linux中如何将域名和地址进行转换的函数。

#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);

struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*主机 IP 地址类型 IPv4 为 AF_INET*/
int h_length; /*主机 IP 地址字节长度,对于 IPv4 是 4 字节,即 32 位*/
char **h_addr_list; /*主机的 IP 地址列表*/
};

#define h_addr h_addr_list[0] /*保存的是 ip地址

函数 gethostbyname():用于将域名(www.baidu.com)或主机名转换为 IP 地址。参数 hostname 指向存放域名或主机名的字符串。
函数 gethostbyaddr():用于将 IP 地址转换为域名或主机名。参数 addr 是一个 IP 地址,此时这个ip 地址不是普通的字符串,而是要通过函数 inet_aton()转换。len 为 IP 地址的长度,AF_INET 为 4。family 可用 AF_INET:Ipv4 或 AF_INET6:Ipv6。

示例 4-2-2:将www.baidu.com转换为ip地址

#include<stdio.h>
#include<sys/socket.h>
#include<netdb.h>

int main()
{
	char *ptr = "www.baidu.com";
	char **pptr;	
	char str[32] = {'\0'};
	struct hostent *hptr;	//定义一个hostent结构体
	if((hptr = gethostbyname(ptr)) == NULL){	//尝试获取www.baidu.com的地址结构
		printf("%s转换ip失败\n",ptr);
		return 0;
	}
	printf("规范主机名为:%s\n",hptr->h_name);
	for(pptr = hptr->aliases;*pptr!=NULL;pptr++){
		printf("域名别称:%s\n",*pptr);
	}
	return 0;
}

3 socket编程

简单介绍一下传输层的协议有两种,TCP和UDP。TCP面向连接,UDP无连接。TCP开销大,UDP开销小。TCP适合做需要稳定传输的工作,比如文件传输、数据传输等功能,数据可靠性高,UDP适合做视频会议、语音聊天等功能,数据包丢失影响也不大。

3.1 TCP协议流程图

在这里插入图片描述

3.1.1 服务器端所用到所有函数介绍:

  1. 头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include <stdio.h>
#include <stdlib.h>
  1. socket函数:生成套接字描述符
    原型:int socket(int domain,int type,int protocol);通常为 0. 返回值:成功则返回套接口描述符,失败返回-1。
    参数:
    domain:{ AF_INET:Ipv4 网络协议 AF_INET6:IPv6 网络协议}
    type:{ tcp:SOCK_STREAM udp:SOCK_DGRAM}
    protocol:指定 socket 所使用的传输协议编号。

  2. bind函数:用来绑定端口号和IP地址,使其相关联
    函数int bind(int sockfd,struct sockaddr * my_addr,int addrllen);返回值:成功则返回 0,失败返回-1
    参数:
    sockfd:为前面 socket 的返回值。
    my_addr:为结构体指针变量
    addrlen:sockaddr 的结构体长度。通常是计算 sizeof(struct my_addr);

对于不同的socket fomain定义了一个通用的数据结构,sockaddr结构体会因为使用的不同的socketdomain有不同的定义。

//例如使用 AF_INET domain,其 socketaddr 结构定义便为
struct sockaddr_in //常用的结构体
{
	unsigned short int sin_family; //即为 sa_family AF_INET
	uint16_t sin_port; //为使用的 port 编号
	struct in_addr sin_addr; //为 IP 地址
	unsigned char sin_zero[8]; //未使用
};
struct in_addr
{
	uint32_t s_addr;
};
  1. listen 函数:使服务器的这个接口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有链接请求,端口就会接受这个连接。
    函数:int listen(int sockfd,int backlog); ,成功则返回 0,失败返回-1
    参数:
    sockfd:为前面的socket的返回值,就是套接字的描述符
    backlog:指定同时处理的最大连接要求。最大设为128。

  2. accept函数:接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当 accept 函数接受一个连接时,会返回一个新的 socket 标识符,以后的数据传输和读取就要通过这个新的 socket 编号来处理,原来参数中的 socket 也可以继续使用,继续监听其它客户机的连接请求。
    函数:int accept(int sfd,struct sockaddr * addr,int * addrlen); ,成功则返回新的 socket 处理代码 new_fd,失败返回-1
    参数:
    sfd:套接字描述符
    addr:结构体指针变量,和bind的结构体是同种类型,系统内核会自动把远程主机的信息(地址和端口号)保存在结构体中。
    addrlen:结构体长度

  3. recv函数,用新的套接字来接收远端主机传来的数据,并把数据存到由参数 buf 指向的内存空间
    原型:int recv(int sockfd,void *buf,int len,unsigned int ,成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1。
    参数:
    socketfd:用来传输文件的套接字描述符,就是accept接受链接后返回的套接字描述符。
    buf:缓冲区地址
    len:缓冲区长度
    flag:一般置为0

  4. send函数,用新的套接字发送数据给指定的远端主机
    原型:int send(int sfd,const void * msg,int len,unsigned int flags);,成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1
    参数:
    sfd:accept返回的传输数据套接字描述符
    msg:发送的数据
    len:发送数据长度
    flag:一般为0

  5. close函数,文件部分close可以关闭一个文件,网络编程部分close可以关闭连接
    原型:int close(int fd);,若文件顺利关闭则返回 0,发生错误时返回-1
    参数:
    fd:此处可以为socket描述符sfd

3.1.2 客户端所用到的函数介绍

  1. connect函数:用来请求连接远程服务器,将参数 sockfd 的 socket 连至参数 serv_addr 指定的服务器IP 和端口号。
    原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen) 成功则返回 0,失败返回-1
    参数:
    sockfd:用于连接的套接字描述符
    serv_addr:结构体指针变量,存储需要连接的远程服务器的IP和端口号
    addrlen:结构体变量长度

3.1.3 示例:TCP传输数据

通用头文件head.h

#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <grp.h>
#include <pwd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/time.h>
#include <strings.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <signal.h>
#include <pthread.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define ARGS_CHECK(argc,num) {if(argc!=num) {printf("error args\n");return -1;}}
#define ERROR_CHECK(ret,retval,func_name) {if(ret==retval) {printf("errno=%d,",errno);fflush(stdout);perror(func_name);return -1;}}
#define THREAD_ERR_CHECK(ret,func_name) {if(ret!=0) {printf("%s failed,%d %s\n",func_name,ret,strerror(ret));return -1;}}

服务器端代码:server.c

#include"head.h"

int main(int argc,char** argv)
{
    ARGS_CHECK(argc,3);
    int sfd;
    sfd = socket(AF_INET,SOCK_STREAM,0);	//AF_INET表示IPV4,SOCK_STREAM表示TCP
    ERROR_CHECK(sfd,-1,"socket");
    printf("socket文件描述符%d\n",sfd);
    struct sockaddr_in ser_addr;		//定义一个服务端描述结构体
    bzero(&ser_addr,sizeof(ser_addr));	//清空结构体
    ser_addr.sin_family = AF_INET;		//设置IPV4通信
    ser_addr.sin_addr.s_addr = inet_addr(argv[1]);	//设置服务端地址:把ip的点分十进制转换为网络字节序
    ser_addr.sin_port = htons(atoi(argv[2]));		//设置服务端的通信端口:把端口号转换为网络字节序
    //开始绑定:用服务端描述结构体和socket描述符绑定 
    int ret = bind(sfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)); 
    ERROR_CHECK(ret,-1,"bind");
    //开始监听,端口就开启了
    ret=listen(sfd,10);
    ERROR_CHECK(ret,-1,"listen");
    int new_fd;	//这个是传输数据的socket描述符
    struct sockaddr_in client_addr;		//客户端描述结构体
    bzero(&client_addr,sizeof(client_addr));	//清空结构体
    socklen_t addr_len = sizeof(client_addr);	//设置长度
    //accept自动完成三次握手,如果没有成功,accept会阻塞
    new_fd = accept(sfd,(struct sockaddr*)&client_addr,&addr_len);
    ERROR_CHECK(new_fd,-1,"accept");
    printf("client ip = %s,port = %d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
    //服务器端先接收数据
    char buf[128] = {0}; 	//设置缓冲区
    ret = recv(new_fd,buf,sizeof(buf),0);	//接收数据到缓冲区
    ERROR_CHECK(ret,-1,"recv");
    printf("我是服务端,接收的数据是%s\n",buf);
    //服务器发送数据
    send(new_fd,"world",5,0);	//发送数据给客户端socket描述符
    close(new_fd);	//关闭数据传输
    close(sfd);    	//关闭通信控制                 	                                                
    return 0;
}

客户端代码:client.c

#include"head.h"

int main(int argc,char** argv)
{
	ARGS_CHECK(argc,3);
	int sfd;
	sfd = socket(AF_INET,SOCK_STREAM,0);	//同上初始化是一个socket描述符,IPV4和TCP协议的
	ERROR_CHECK(sfd,-1,"socket");
    printf("sfd=%d\n",sfd);
	struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));//清空
    ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
	//客户端连接服务器
    int ret=connect(sfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
    ERROR_CHECK(ret,-1,"connect");
    //客户端先发数据
    ret=send(sfd,"hello",5,0);
    ERROR_CHECK(ret,-1,"send");
    //客户端接收数据
    char buf[128]={0};
    recv(sfd,buf,sizeof(buf),0);
    printf("我是客户端,接收的数据是=%s\n",buf);
    close(sfd);
	return 0;
}

本机测试:
在这里插入图片描述

3.1.4 socket优化方法

  1. 多线程传输(开销较大,不常用)
  2. 调用fcntl 函数将sockfd设置为非阻塞模式
  3. 多路选择select 函数实现实时聊天

3.1.5 客户端服务器聊天通信

使用select 监控实现实时聊天的效果。
相同功能的注释简写,头文件和上面的一样。

服务器端server.c

#include"head.h"

int main(int argc,char** argv)
{
	ARGS_CHECK(argc,3);
	int sfd;
	//初始化socket标识符
	sfd = socket(AF_INET,SOCK_STREAM,0);
	ERROR_CHECK(sfd,-1,"socket");
	printf("sfd=%d\n",sfd);
	//定义服务端描述结构体
	struct sockaddr_in ser_addr;
	bzero(&ser_addr,sizeof(ser_addr));	
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_addr.s_addr = inet_addr(argv[1]);
	ser_addr.sin_port = htons(atoi(argv[2]));
	//网络标识符和服务器结构体绑定
	int ret = bind(sfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
	ERROR_CHECK(ret,-1,"bind");
	//对网络标识符进行监听
	ret = listen(sfd,10);
	ERROR_CHECK(ret,-1,"listen");
	int new_fd;	//定义一个传输文件的标识符
	struct sockaddr_in client_addr;	//定义一个客户端描述结构体
	bzero(&client_addr,sizeof(client_addr));
	socklen_t addr_len = sizeof(client_addr);
	//用accept完成三次握手,如果没有连接,accept会阻塞
	new_fd = accept(sfd,(struct sockaddr*)&client_addr,&addr_len);
	ERROR_CHECK(new_fd,-1,"accept");
	printf("client ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
	//编写即时聊天
	char buf[128] = {0};	//缓冲区
	fd_set rdset;	//建立一个文件描述集合
	while(1){
		//初始化文件描述集合,并将标准输入输出和网络传输描述符加入集合中,用于select监控
		FD_ZERO(&rdset);
		FD_SET(STDIN_FILENO,&rdset);
		FD_SET(new_fd,&rdset);
		//用select监控rdset集合中所有文件描述符,必须+1
		ret = select(new_fd+1,&rdset,NULL,NULL,NULL);
		//如果标准输入输出是否准备好,则清空缓冲区准备读
		if(FD_ISSET(STDIN_FILENO,&rdset){
			bzero(buf,sizeof(buf));
			//如果从标准输入中读取的内容为空,则退出聊天
			ret = read(STDIN_FILENO,buf,sizeof(buf));
			if(!ret){
				printf("服务器:想结束聊天\n");
				break;
			}
			//如果读入内容不为空,则将标准输入的内容发送给客户端
			send(new_fd,buf,strlen(buf)-1,0);
		}
		//如果网络传输描述符是否准备好,也清空缓冲区
		if(FD_ISSET(new_fd,&rdset)){
			bzero(buf,sizeof(buf));
			//如果从客户端接收到的字符数为0,则退出聊天
			ret = recv(new_fd,buf,sizeof(buf),0);
			ERROR_CHECK(ret,-1,"recv");
			if(!ret){
				printf("对方离开了聊天\n");
				break;
			}
			printf("客户端发送:%s\n",buf);
		}
	}
	close(new_fd);	
	close(sfd);
	return 0;
}
#include"head.h"

int main(int argc,char** argv)
{
	ARGS_CHECK(argc,3);
	int sfd;
	sfd = socket(AF_INET,SOCK_STREAM,0);	//同上初始化是一个socket描述符,IPV4和TCP协议的
	ERROR_CHECK(sfd,-1,"socket");
    printf("sfd=%d\n",sfd);
	struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));//清空
    ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
    //客户端连接服务器
    int ret=connect(sfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
    ERROR_CHECK(ret,-1,"connect");
    //即时聊天
    char buf[128] = {0};
    fd_set rdset;
    while(1){
		//初始化文件描述集合,并将标准输入输出和网络传输描述符加入集合中,用于select监控
		FD_ZERO(&rdset);
		FD_SET(STDIN_FILENO,&rdset);
		FD_SET(new_fd,&rdset);
		//监控集合中哪一个描述符就绪
        ret=select(sfd+1,&rdset,NULL,NULL,NULL);
        //如果标准输入输出是否准备好,则清空缓冲区准备读
		if(FD_ISSET(STDIN_FILENO,&rdset){
			bzero(buf,sizeof(buf));
			//如果从标准输入中读取的内容为空,则退出聊天
			ret = read(STDIN_FILENO,buf,sizeof(buf));
			if(!ret){
				printf("服务器:想结束聊天\n");
				break;
			}
			//如果读入内容不为空,则将标准输入的内容发送给客户端
			send(new_fd,buf,strlen(buf)-1,0);
		}
		//如果网络传输描述符是否准备好,也清空缓冲区
		if(FD_ISSET(new_fd,&rdset)){
			bzero(buf,sizeof(buf));
			//如果从客户端接收到的字符数为0,则退出聊天
			ret = recv(new_fd,buf,sizeof(buf),0);
			ERROR_CHECK(ret,-1,"recv");
			if(!ret){
				printf("对方离开了聊天\n");
				break;
			}
			printf("服务端发送:%s\n",buf);
		}
	}
	close(sfd);
	return 0;
}

在这里插入图片描述

3.2 UDP协议流程图

在这里插入图片描述

3.2.1 UDP用到函数介绍:

  1. sendto函数,返回实际发送的数据字节长度或在出现发送错误时返回-1。
    函数int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
    该函数比 send()函数多了两个参数,to 表示目地机的 IP 地址和端口号信息,而 tolen 常常被赋值为sizeof (struct sockaddr)。

  2. recvfrom函数,返回接收到的字节数或当出现错误时返回-1,并置相应的 errno。
    函数int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen); from 是一个 struct sockaddr 类型的变量,该变量保存连接机的 IP 地址及端口号。fromlen 常置为 sizeof(struct sockaddr)。

3.2.2 示例:使用UDP连接传输数据

UDP服务端server.c

#include"head.h"

int main(int argc,char** argv)
{
	ARGS_CHECK(argc,3);
    int sfd=socket(AF_INET,SOCK_DGRAM,0);//SOCK_DGRAM表示使用UDP协议
    ERROR_CHECK(sfd,-1,"socket");
    struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));//清空
    ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
    int ret=bind(sfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
    ERROR_CHECK(ret,-1,"bind");
    char buf[128]={0};	//设置缓冲区
    struct sockaddr_in client_addr;//客户端的socket结构体
    bzero(&client_addr,sizeof(client_addr));
    socklen_t addr_len=sizeof(client_addr);
    //服务器先recvfrom
    ret=recvfrom(sfd,buf,sizeof(buf),0,(struct sockaddr*)&client_addr,&addr_len);
    printf("client ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
    printf("%s\n",buf);
    sendto(sfd,"world",5,0,(struct sockaddr*)&client_addr,sizeof(client_addr));
    close(sfd);
	return 0;
}

客户端Client.c

#include"head.h"

int main(int argc,char** argv)
{
    ARGS_CHECK(argc,3);
    int sfd=socket(AF_INET,SOCK_DGRAM,0);	//创建socket描述符
    ERROR_CHECK(sfd,-1,"socket");
    struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));//清空
    ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
    sendto(sfd,"hello",5,0,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
    char buf[128]={0};	//缓冲区
    recvfrom(sfd,buf,sizeof(buf),0,NULL,NULL);
    printf("%s\n",buf);
    close(sfd);
    return 0;
}

在这里插入图片描述

3.2.3 示例使用UDP实现即时聊天

服务端server.c

#include"head.h"

int main(int argc,char** argv)
{
	ARGS_CHECK(argc,3);
	int sfd = socket(AF_INET,SOCK_DGRAM,0);
	ERROR_CHECK(sfd,-1,"socket");
	struct sockaddr_in ser_addr;
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_addr.s_addr = inet_addr(argv[1]);
	ser_addr.sin_port = htons(atoi(argv[2]));
	int ret = bind(sfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
	ERROR_CHECK(ret,-1,"bind");
	char buf[128] = {0};
	struct sockaddr_in client_addr;	//客户端的socket
	bzero(&client_addr,sizeof(client_addr));
	socklen_t addr_len = sizeof(client_addr);
	//服务器先等待接收信息
	ret = recvfrom(sfd,buf,sizeof(buf),0,(struct sockaddr*)&client_addr,&addr_len);
	printf("client ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
	fd_set rdset;//文件描述符集合
	while(1){
		FD_ZERO(&rdset);
        FD_SET(STDIN_FILENO,&rdset);
        FD_SET(sfd,&rdset);
        //监控哪一个描述符就绪
        ret=select(sfd+1,&rdset,NULL,NULL,NULL);
        if(FD_ISSET(STDIN_FILENO,&rdset)){
            //读取标准输入,并发给对方
            bzero(buf,sizeof(buf));
            int ret = read(STDIN_FILENO,buf,sizeof(buf));
			sendto(sfd,buf,strlen(buf)-1,0,(struct sockaddr*)&client_addr,sizeof(client_addr));
        }
        if(FD_ISSET(sfd,&rdset)){
            //sfd可读,对方发了数据,读取
            bzero(buf,sizeof(buf));
            ret=recvfrom(sfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&client_addr,&addr_len);
			printf("客户端发送:%s\n",buf);
        }
	}
	return 0;
}

客户端client.c

#include"head.h"

int main(int argc,char** argv)
{
    ARGS_CHECK(argc,3);
    int sfd=socket(AF_INET,SOCK_DGRAM,0);
    ERROR_CHECK(sfd,-1,"socket");
    struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));//清空
    ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
    sendto(sfd,"hello",5,0,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
    char buf[128]={0};
    fd_set rdset;
    int ret;
    while(1)
    {
        //清空集合并写入要监控的描述符
        FD_ZERO(&rdset);
        FD_SET(STDIN_FILENO,&rdset);
        FD_SET(sfd,&rdset);
        //监控哪一个描述符就绪
        ret=select(sfd+1,&rdset,NULL,NULL,NULL);
        if(FD_ISSET(STDIN_FILENO,&rdset))
        {
            //读取标准输入,并发给对方
            bzero(buf,sizeof(buf));
            ret = read(STDIN_FILENO,buf,sizeof(buf));
            sendto(sfd,buf,strlen(buf)-1,0,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
        }
        if(FD_ISSET(sfd,&rdset))
        {
            //sfd可读,对方发了数据,读取
            bzero(buf,sizeof(buf));
            ret=recvfrom(sfd,buf,sizeof(buf),0,NULL,NULL);
			printf("服务端发送:%s\n",buf);
        }
    }
    close(sfd);
    return 0;
}

在这里插入图片描述

3.3 设置socket套接字选项函数setsockopt的用法

#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数:
sockfd:套接字描述符
optname:需要设置的选项名
optval:指针,指向存放选项值的缓冲区
optlen:optval缓冲区长度
level:选择定义的层次:支持 SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP 和 IPPROTO_IPV

  • SOL_SOCKET可设置的选项
选项说明数据类型
SO_BROADCAST允许发送广播数据int
SO_DEBUG允许调试int
SO_DONTROUTE不查找路由int
SO_ERROR获得套接字错误int
SO_KEEPALIVE保持连接int
SO_LINGER延迟关闭连接struct linger
SO_OOBINLINE带外数据放入正常数据流int
SO_RCVBUF接收缓冲区大小int
SO_SNDBUF发送缓冲区大小int
SO_RCVLOWAT接收缓冲区下限int
SO_SNDLOWAT发送缓冲区下限int
SO_RCVTIMEO接收超时struct timeval
SO_SNDTIMEO发送超时struct timeval
SO_REUSEADDR允许重用本地地址和端口int
SO_TYPE获得套接字类型int
SO_BSDCOMPAT与 BSD 系统兼容int
  • IPPROTO_IP可设置的选项
选项说明数据类型
IP_HDRINCL在数据包中包含 IP 首部int
IP_OPTINOSIP首部选项int
IP_TOS服务类型
IP_TTL生存时间int
  • IPPRO_TCP可设置的选项
选项说明数据类型
TCP_MAXSEGTCP 最大数据段的大小int
TCP_NODELAY不使用 Nagle 算法int

3.4 单播、广播、组播概念

对于单播而言,单播用于两个主机之间的端对端通信。
对于广播而言,广播用于一个主机对整个局域网上所有主机上的数据通信。
对于多播而言,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

  • 多播的程序设计也要使用 setsockopt()函数和 getsockopt()函数来实现。其中对于 setsockopt 的第二个参数 level 不再是 SOL_SOCKET,而是 IPPROTO_IP;第三个参数 optname 常用选项如下:
  1. IP_ADD_MEMBERSHIP 和 IP_DROP_MEMBERSHIP 加入或者退出一个组播组,通过选项IP_ADD_MEMBERSHIP 和 IP_DROP_MEMBERSHIP,对一个结构 struct ip_mreq 类型的变量进行控制。
struct ip_mreq
{
	struct in_addr imr_multiaddr; /*加入或者退出的多播组 IP 地址*/
	struct in_addr imr_interface; /*加入或者退出的网络接口 IP 地址,本机 IP*/
};
  1. 选项 IP_ADD_MEMBERSHIP 用于加入某个多播组,之后就可以向这个多播组发送数据或者从多播组接收数据。此选项的值为 mreq 结构,成员 imr_multiaddr 是需要加入的多播组 IP 地址,成员 imr_interface是本机需要加入多播组的网络接口 IP地址。
struct ip_mreq mreq;
memset(&mreq, 0, sizeof(struct ip_mreq));
mreq.imr_interface.s_addr = INADDR_ANY;
mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1");
if(-1 == setsockopt(sfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(struct ip_mreq)))
{
	perror("setsockopt");
	exit(-1);
}
  1. 选项 IP_ADD_MEMBERSHIP 每次只能加入一个网络接口的 IP 地址到多播组,但并不是一个多播组仅允许一个主机 IP 地址加入,可以多次调用 IP_ADD_MEMBERSHIP 选项来实现多个 IP 地址加入同一个广播组,或者同一个 IP 地址加入多个广播组。
  2. 选项 IP_DROP_MEMBERSHIP 用于从一个多播组中退出。

3.5 DDos网络攻击(SYN Flooding 泛洪网络攻击)

Linux内核中如何对SYN进行处理:首先收到SYN包,SYN包请求等待连接成为半连接状态,内核会建立一个半连接队列,将SYN请求加入队列,接着发送ack即syn给客户端,等待客户端的回应,完成三次握手建立连接。

利用三次握手的特性,此时服务端收到了来自攻击者很多大量的SYN包,内核的队列就会占用很大空间,导致后来到的正常连接就无法建立连接,这种攻击方式叫做DOS攻击,又叫泛洪攻击,非常形象,就是发送洪水一般的SYN包,将服务器淹没。
在这里插入图片描述

  • SYN Flood 攻击防护手段
  1. 增加半连接队列长度
  2. 减少SYN+ACK的重传次数
  3. 启用syn cookie屏蔽伪请求

第一种和第二种没有办法根除问题,第三种可以有效防御,但是缺陷很明显,添加额外的字段进行验证,必然会有空间和时间上的损耗。

3.6 文件描述符属性修改,以及文件描述符的传递

  1. fcntl函数:用于改变已打开的文件描述符的性质。
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);

参数fd:打算设置的文件描述符
参数cmd:操作的指令

指令功能
F_DUPFD用来查找大于或等于参数 arg 的最小且仍未使用的文件描述词,并且复制参数 fd 的文件描述词。执行成功则返回新复制的文件描述词。新描述符与 fd 共享同一文件表项,但是新描述符有它自己的一套文件描述符标志,其中 FD_CLOEXEC 文件描述符标志被清除。请参考 dup2()。
F_GETFD取得 close-on-exec 旗标。若此旗标的 FD_CLOEXEC 位为 0,代表在调用 exec()相关函数时文件将不会关闭。
F_SETFD设置 close-on-exec 旗标。该旗标以参数 arg 的 FD_CLOEXEC 位决定。
F_GETFL取得文件描述词状态旗标,此旗标为 open()的参数 flags。
F_SETFL设置文件描述词状态旗标,参数 arg 为新旗标,但只允许 O_APPEND、O_NONBLOCK和 O_ASYNC 位的改变,其他位的改变将不受影响。
  1. Socketpair函数int socketpair(int domain, int type, int protocol, int sv[2]); 前面 3 个参数参照 socket,domain 变为AF_LOCAL,sv[2],放置 fd[2]。
  2. Sendmsg函数ssize_t sendmsg (int sfd, const struct msghdr *msg, int flags); 系统调用,用于发送消息到另一个套接字。sfd套接字描述符,flags和上述相同。结构体struct msghdr:
struct msghdr {
	void *msg_name;	//套接字地址
	socklen_t msg_namelen;	//套接字地址长度
	struct iovec *msg_iov;	//IO向量地址
	size_t msg_iovlen;		//IO向量数组大小
	void *msg_control;		//附属数据区
	size_t msg_controllen;	//数据区大小
	int msg_flags;
};
  1. Recvmsg函数int recvmsg(int sfd, struct msghdr *msg, unsigned int flags); 接收数据
  2. Writev函数,一次写入多个buf内容
ssize_t writev(int sfd, const struct iovec *iov, int iovcnt)
struct iovec {
	void *iov_base; /* Starting address */
	size_t iov_len; /* Number of bytes to transfer */
};
  1. Readv函数,ssize_t readv(int fd, const struct iovec *iov, int iovcn一次读取多个buf内容
  2. Cmsg函数,size_t CMSG_LEN(size_t length);用来设定*msg_control指针

3.6.1 fcntl 非阻塞示例

非阻塞的意思就是不等待的意思,如果进程在规定时间没有从缓冲区拿到数据就会报错。
如此案例,如果不输入内容,5秒后就会返回。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>

int fcntl_nonblock(int fd)
{
    int status=fcntl(fd,F_GETFL);//拿出标准输入所有的属性
    status=status|O_NONBLOCK;//修改其为非阻塞
    int ret=fcntl(fd,F_SETFL,status);//修改fd的状态
    ERROR_CHECK(ret,-1,"fcntl");
    return 0;
}

//修改标准输入为非阻塞
int main()
{
    sleep(5);
    char buf[128]={0};
    fcntl_nonblock(STDIN_FILENO);//将标准输入输出改为非阻塞
    int ret=read(STDIN_FILENO,buf,sizeof(buf));	
    printf("ret=%d,buf=%s,errno=%d\n",ret,buf,errno);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值