网络编程socket

计算机网络的定义

  • 以功能完善的网络软件及通信协议实现资源共享和信息传递的系统
  • 一传输信息为基本目的,用通信线路和通信设备将多个计算机链接起来的计算机系统的集合

计算机网络的分类

1.作用范围分类

  • 广域网
  • 城域网
  • 局域网
  • 个人区域网

2.使用者分类

  • 公用网
  • 专用网

互联网的组成

从工作方式上看:可以划分为两大块:

  • 边缘部分:由所有链接在互联网上的主机组成,由用户直接使用,用来进行通信和资源共享
  • 核心部分:由大量网络和连接这些网络的路由器组成,为边缘部分提供服务

边缘部分

处在互联网边缘部分的就是连接在互联网上的所有主机,这些主机又称为端系统

端系统的分类:

  • 大的端系统:非常昂贵的大型计算机或服务器
  • 个人电脑,智能手机,网络摄像头等;

分层与体系结构

在这里插入图片描述

巧记:巫术表传会飙鹰

计算机网络体系结构的形成

法律上的国际标准:OSI协议

事实上的国际标准:TCP/IP协议

协议与划分层次

协议的两种形式:

  • 文字描述
  • 程序代码

各层完成的主要功能

  • 差错控制:使相应层次对等方的通信更加可靠。
  • 流量控制:发送端的发送速率必须使接收端来得及接收,不要太快。
  • 分段和重装:发送端将要发送的数据块划分为更小的单位,在接收端将其还原。
  • 复用和分用:发送端几个高层会话复用一条低层的连接,在接收端再进行分用。
  • 连接建立和释放:交换数据前先建立一条逻辑连接,数据传送结束后释放连接。

计算机网络的体系结构

是计算机网络的各层及其协议的集合,就是这个计算机网络及其构件所应完成的功能的精确定义

OSI七层体系结构

  • 应用层 -网络服务与最终用户的一个接口
  • 表示层 -数据的表示、安全、压缩
  • 会话层 -建立、管理、终止会话
  • 运输层(传输层) -定义传输数据的协议端口号、以及流控和差错校验(网关)
  • 网络层 -进行逻辑地址寻址、差错校验等功能(网络层)(路由)
  • 数据链路层 -建立逻辑链接、进行硬件地址寻址、差错校验等功能(mac地址)(交换机)
  • 物理层 -建立、维护、断开物理链接(网卡)

应用层、表示层、会话层(高层):负责主机之间的数据传输

运输层、网络层、数据链路层、物理层(底层):负责网络之间的数据传输

TCP/IP协议

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

TCP/IP协议族

在这里插入图片描述

具有五层协议的体系结构

在这里插入图片描述

各层的主要功能

应用层

  • 任务:通过应用进程间的交互来完成特定网络应用
  • 协议: 定义的是应用进程间通信和交互的规则
  • 把应用层交互的数据单元称为报文(message)
  • 例如:DNS,HTTP,SMTP

传输层

  • 任务:负责向两台主机中进程之间的通信提供通用的数据传输服务。具有复用和分用的功能。
  • 主要使用两种协议:

​ 传输控制协议 TCP

​ 用户数据报协议 UDP

  • 考虑端口号的正确性

网络层

  • 互联网使用的网络层协议是无连接的网际协议IP和许多种路由选择协议因此护粮网的网络层也叫做网际层或者IP层
  • IP协议分组也叫做IP数据报,或者称为数据报;
  • 考虑IP包头的正确性

数据链路层

  • 任务:实现两个相邻节点之间的可靠通信
  • 在两个相邻节点间大的链路上传送
  • 如发现有差错(bit差错),就简单的丢弃出错的帧
  • 如果需要改正出现的差错,就要采用可靠传输协议来纠正出现的差错,这种方法会是数据链路层协议复杂
  • 考虑数据比特位的正确性

物理层

  • 任务:实现比特(0或1)的传输
  • 确定链接电缆的插头应当有多少根引脚,以及各引脚应如何连接
  • 注意:传递信息所利用的一些物理媒体,如双绞线,同轴电缆,光缆,无线通道等,并不在物理层协议之内,而是在物理层协议的下面。

互联网中客户-服务器工作方式

在这里插入图片描述

同时为多个客户端进程提供服务

在这里插入图片描述

IPV4

IPV4地址及表示方法

IP地址采用2级结构

2个字段:网络号和主机号

IP地址::={<网络号>,<主机号>}

IP地址在整个互联网范围内是唯一的;IP地址指明了链接到某个网络上的一个主机;

IP的分类

在这里插入图片描述

  • A类IP:0~127
  • B类IP:128~191
  • C类IP:192~224

多归属主机

  • 任意一个IP地址我们都可以迅速的得出类别,并计算得出网络号

  • 当一个主机通过两个网卡同时连接到两网络时,也就是该主机同时拥有两个IP地址,该主机被称为多归属主机、

  • 一个路由器至少连接到两个不同的网络,一个路由器至少拥有两个IP地址

一般不使用的特殊的 IP 地址

网络号主机号源地址使用目的地址使用代表的意思
00可以不可在本网络上的本主机
0X可以不可在本网络上主机号为 X 的主机
全 1全 1不可可以只在本网络上进行广播(各路由器均不转发)
Y全 1不可可以对网络号为 Y 的网络上的所有主机进行广播
127非全 0 或全 1 的任何数可以可以用于本地软件环回测试

分类的 IP 地址的优点和缺点

优点:

  • 管理简单;
  • 使用方便;
  • 转发分组迅速;
  • 划分子网,灵活地使用

缺点

  • 设计上不合理:
  • 大地址块,浪费地址资源;
  • 即使采用划分子网的方法,也无法解决 IP 地址枯竭的问题。

划分子网

  • 随着加入互联网的组织数量的迅速增加,IP地址面临被分配完的危险;

  • 为了解决上述问题,IETF提出了划分子网的编址改进方案;

  • 三级IP地址:网络号、子网号和主机号

IP地址::={<网络号>,<子网号>,<主机号>}

这种方式仍然无法解决问题,因为C类地址IP主机号太少无法满足要求

无分类编制CIDR

CIDR:无分类域间路由选择。

消除了传统的A类、B类和C类地址以及划分子网的概念,可以更加有效地分配IPv4的地址空间,但无法解决IP地址枯竭的问题。

IP地址:={<网络前缀>,<主机号>}

网络前缀

斜线记法:例如:128.14.35.7/20:前 20 位是网络前缀

地址块

CIDR 把网络前缀都相同的所有连续的 IP 地址组成一个 CIDR 地址块。
一个 CIDR 地址块包含的 IP 地址数目,取决于网络前缀的位数。

在这里插入图片描述

注意:

128.14.35.7/20是 IP 地址,同时指明了网络前缀为 20 位。该地址是 128.14.32.0/20 地址块中的一个地址。
128.14.32.0/20是包含有多个 IP 地址的地址块,同时也是这个地址块中主机号为全 0 的 IP 地址。
128.14.35.7是 IP 地址,但未指明网络前缀长度,不知道其网络地址。
128.14.32.0不能指明一个网络地址,因为无法知道网络前缀是多少。

子网掩码

又称地址掩码;位数32位;让机器从 IP 地址迅速算出网络地址;

/20 地址块的地址掩码:
11111111 11111111 11110000 00000000
点分十进制记法:255.255.240.0
       CIDR 记法:255.255.240.0/20

默认地址掩码

在这里插入图片描述

网络地址 = (二进制的IP地址) AND (地址掩码)

IPV6

IPV6地址及表示方式

在IPV6中,每个地址占 128 位,地址空间大于 3.4  1038

冒号十六进制记法:

在这里插入图片描述

零压缩

一串连续的零可以用一对冒号取代;

在这里插入图片描述

注意:在任一地址中,只能使用一次零压缩

端口号

运输层的作用

在这里插入图片描述

屏蔽作用

运输层向高层用户屏蔽了下面网络核心的细节(如网络拓扑、所采用的路由选择协议等),使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道;

可靠信道与不可靠信道

在这里插入图片描述

运输层的两个主要协议

  • 用户数据报协议 UDP (User Datagram Protocol)
  • 传输控制协议 TCP (Transmission Control Protocol)

在这里插入图片描述

运输协议数据单元

  • 两个对等运输实体在通信时传送的数据单位叫作运输协议数据单元 TPDU (Transport Protocol Data Unit)。
  • TCP 传送的数据单位协议是 TCP 报文段 (segment)。
  • UDP 传送的数据单位协议是 UDP 报文或用户数据报。

UDP 与 TCP 的区别

UDP

  • 传送数据之前不需要先建立连接。
  • 收到 UDP 报后,不需要给出任何确认。
  • 不提供可靠交付,但是一种最有效的工作方式。

TCP

  • 提供可靠的、面向连接的运输服务。
  • 不提供广播或多播服务。
  • 开销较多。

使用 UDP 和 TCP 的典型应用和应用层协议

在这里插入图片描述

运输层的端口

在这里插入图片描述

  • 复用:应用进程都可以通过运输层再传送到 IP 层(网络层)

  • 分用:运输层从 IP 层收到发送给应用进程的数据后,必须分别交付给指明的各应用进程

端口号

进程的创建和撤销都是动态的,因此发送方几乎无法识别其他机器上的进程。
我们往往需要利用目的主机提供的功能来识别终点,而不需要知道具体实现这个功能的进程是哪一个。
有时我们会改换接收报文的进程,但并不需要通知所有的发送方。

解决方法:在运输层使用协议端口号 (protocol port number),或通常简称为端口 (port)。把端口设为通信的抽象终点。

在这里插入图片描述

TCP/IP 运输层端口的标志

  • 端口用一个 16 位端口号进行标志,允许有 65,535 个不同的端口号。
  • 端口号只具有本地意义,只是为了标志本计算机应用层中的各进程。
  • 在互联网中,不同计算机的相同端口号没有联系。

两个计算机中的进程要互相通信,不仅必须知道对方的端口号,而且还要知道对方的 IP 地址

两大类、三种类型的端口

在这里插入图片描述

常用的熟知端口

在这里插入图片描述

BSD端口

  • 0不使用,1-1023为系统端口,也叫BSD保留端口。

  • 0-1023: BSD保留端口,也叫系统端口,这些端口只有系统特许的进程才能使用

  • 1024~65535为用户端口,其中:

    • 1024-5000: BSD临时端口,一般的应用程序使用1024到4999来进行通讯;
    • 5001-65535:BSD服务器(非特权)端口,用来给用户自定义端口。
  • IANA建议49152至65535作为“动态或私有端口”。

  • 许多Linux内核使用32768至61000范围。配置文件

    cat /proc/sys/net/ipv4/ip_local_port_range
    

    有当前系统设定。

字节序

字节序

定义:

字节序是指多字节数据在计算机内存中存储或者网络传输是各字节的存储顺序,分为

  • 大端字节序 - 先存储高位后存储低位
  • 小端字节序 - 先存储底位后存储高位

在这里插入图片描述

主机字节序和网络字节序

一般计算机内部处理中使用到小端字节序,方便运算

在网络传输和文件存储当中使用大端字节序

示例:查看主机字节序

#include <stdio.h>
 
int main(int argc, char *argv[])
{
    unsigned int val32 = 0x11223344;
    unsigned char val8 = *((unsigned char *)(&val32));
    if(val8 = 0x44)
        printf("本机是小端字节序\n");
    else
        printf("本机是大端字节序\n");
    return 0;
}

字节序转换函数

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
//头文件 <arpa/inet.h>
本机转网络网络转本机
32位数据htonlntohl
16位数据htonsntohs

IP地址字节序转换函数

//字符串转32位数据
#include<arpa/inet.h>
//IP地址序转换函数
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp,struct in_addr *addr);
int inet_pton(int af, const char *cp, void *addr);
//32位数据转字符串
#include<arpa/inet.h>
//IP地址序转换函数 这两个函数支持IPv6
char* inet_ntoa(struct in_addr in);
int inet_ntop(int af, const void *addr, char *cp);

socket套接字

socket及TCP的实现框架

socket套接字

在这里插入图片描述

socket常用API介绍

/*创建套接字*/
int socket(int domain,int type,int protocol);
/*绑定通信结构体*/
int bind(int sockfd,const struct sockaddr *addr,socklen_t addelen);
/*监听套接字*/
int listen(int sockfd,int backlog);
/*处理客户端发起的链接,生成新的套接字*/
int accept(int sockfd,struct sockaddr *addr,scoklen_t *addrlen);
/*向服务器发起连接请求*/
int connect(int sockfd,const struct sockaddr* addr,socklen_t adddelen);

参数:

domain: 指定绑定通信结构体的类型;一般写AF_INET

type:套接字类型SOCK_STREAMSOCK_DGRAM

protocol 协议,写0即可

三元组

三元组[IP地址,端口,协议]

  • IP地址:标识计算机
  • 端口号:标识计算机当中的进程
  • 协议:指定数据传输的方式

地址族结构体

//通用地址族结构体
struct sockaddr
{
	sa_family_t sa_family;
	char sa_data[14];
}

//IPv4
struct sockaddr_in
{
  sa_family_t sin_family;/*地址族:AF_INET*/
  in_port_t sin_port;/*网络字节序的端口号*/
  struct in_addr sin_addr;/*IP地址结构体*/
};

struct in_addr
{
  uint32_t s_addr;/*网络字节序的IP地址*/
};

//IPv6 
struct sockaddr_in6
{
  sa_family_t sin6_family;
  in_port_t sin6_port;
  uint32_t sin6_flowinfo;
  struct in6_addr sin6_addr;
  uint32_t sin6_scope_id;
}

struct in6_addr
{
  unisgned char s6_addr[16];
}

成员参数:

sa_family --指定使用那种协议

  • AF_UNIX 使用本地域套接字的地址结构,用于本地通信
  • AF_INET 使用IPv4的通信地址结构
  • AF_INET6 使用IPv6的通信地址结构

套接字的类型

  • 流式套接字(SOCK_STREAM)提供可靠的、面向连接的通信流;它使用TCP,从而保证数据传输的可靠性和顺序性

  • 数据报套接字(SOCK_DGRAM)定义了一种不可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP

  • 原始套接字(SOCK_RAW)允许直接访问底层协议,如IP或ICMP,它功能强大但使用较为不便,主要用于协议开发。

在这里插入图片描述

TCP通信的实现过程(文字叙述)

服务器端

1.建立连接

socket(通信之前,客户和服务器先创建套接字)

  • socket函数创建套接字
  • 创建成功后返回大于0的文件描述符fd失败返回小于0

bind(服务器端调用bind,把端口号和本地IP地址填写到已创建的套接字中)

  • 定义一个sockadddr_in结构体
  • 通过bind函数绑定到fd

listen(服务端调用listen,把套接字设置为被动方式,以方便随时接受客户的服务请求,UDP服务器不使用listen系统调用)

  • listen函数会管理一个队列(长度),来一个服务请求就把它放入这个队列中

accept(以便把远地客户进程发来的连接请求提取出来,UDP服务器不适用accept系统调用)

  • 服务端和客户端联系时,从上述中的队列取出这个请求
  • 成功后返回newfd
  • 和客户端形成一对一的通信

2.数据传送

read

write

3.连接释放

close(通信结束,调用close释放连接和撤销套接字)


客户端

1.建立连接

socket(通信之前,客户和服务器先创建套接字)

  • socket函数创建套接字
  • 创建成功后返回大于0的文件描述符fd失败返回小于0

connect(和远地服务器建立连接)

2.数据传送

read

write

3.连接释放

close(通信结束,调用close释放连接和撤销套接字)

TCP通信的实现过程

在这里插入图片描述

socket函数与通信域

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type, int protocol);
  • domain: 指定通信域(通信地址族);

    • AF_INET: 使用IPv4 互联网协议
    • AF_INET6: 使用IPv6 互联网协议
  • type: 指定套接字类型;

    • TCP唯一对应流式套接字,所以选择SOCK_STREAM(数据报套接字:SOCK_DGRAM)
  • protocol: 指定协议;

    • 流式套接字唯一对应TCP,所以无需要指定协议,设为0即可

bind函数与通信结构体

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • sockfd:socket函数生成的套接字
  • addr:通信结构体
    • sin_port 注意端口号的转换和端口号字符到数字的转换
    • addr.sin_addr.s_addr 注意ip的转换
  • addrlen:通信结构体的长度

在这里插入图片描述

示例:为套接字fd绑定通信结构体addr

addr.sin_family = AF_INET;
addr.sin_port = htons(5001);
addr.sin_addr.s_addr = 0;
bind(fd,(struct sockaddr *)&addr,sizeof(addr));

listen函数与accept函数

/*监听套接字*/
int listen(int sockfd,int backlog);
/*处理客户端发起的连接,生成新的套接字*/
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
  • sockfd:函数socket生成的套接字

  • addr:客户端的地址族信息

    • 存放的是新的套接字中客户端的信息
  • addrlen:地址族结构体的长度

示例:服务端和客户端信息接受

客户端

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include <string.h>

//#define PORT 5001
#define BACKLOG 5
//#define STR "hello world"


int main(int argc,char *argv[])
{
	int fd;
  char buf[BUFSIZ] = {};

	struct sockaddr_in addr;
	if(argc < 3){
		fprintf(stderr, "%s<addr><port>\n", argv[0]);
		exit(0);
	}
	/*创建套接字*/
	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd < 0)
	{
		perror("socket");
		exit(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}
	/*向服务端发起连接请求*/
	if((connect(fd,(struct sockaddr *)&addr,sizeof(addr))) == -1)
	{
		perror("connect");
		exit(0);
	}
	while(1){
		printf("Input->");
		fgets(buf, BUFSIZ, stdin);
		write(fd, buf, strlen(buf) );
	}
	close(fd);
	return 0;
}

服务端

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include <string.h>

通过argv传递
//#define PORT 5001
#define BACKLOG 5


int main(int argc,char *argv[])
{
	char buf[BUFSIZ] = {};//BUFSIZ=8192 BUFSIZ为系统默认的缓冲区大小
	int fd;
	int newfd;
  int ret;
	struct sockaddr_in addr;
  
  /*输入的参数小于3时出错*/
  if(argc < 3)
  {
    fprintf(stderr,"%s<addr><port>\n",argv[0]);//stderr 标准错误流 默认会打印到屏幕上
    exit(0);
  }
	/*创建套接字*/
	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd < 0)
	{
		perror("socket");
		exit(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));//atoi函数 将输入的char转换为int类型的数字
	//addr.sin_addr.s_addr = 0;
	if(inet_aton(argv[1], &addr.sin_addr) == 0)//将输入的IP地址转换
  {
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}
  
	/*地址快速重用*/
  int flag = 1;
  int len  = sizeof(int);
  if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,len) == -1)
  {
    perror("setsockopt");
    exit(1);
  }
  
	/*绑定通信结构体*/
	if((bind(fd,(struct sockaddr *)&addr,sizeof(addr))) == -1)
	{
		perror("bind");
		exit(0);
	}
	/*设置套接字为监听模式*/
	if(listen(fd,BACKLOG) == -1)
	{
		perror("listen");
		exit(0);
	}
	/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
	newfd = accept(fd,NULL,NULL);
	if(newfd < 0)
	{
		perror("accept");
		exit(0);
	}
  
	while(1)
  {
		memset(buf, 0, BUFSIZ);
		ret = read(newfd, buf, BUFSIZ);
		if(ret < 0)
		{
			perror("read");
			exit(0);
		}
		else if(ret == 0)
			break;
		else
			printf("buf = %s\n", buf);
	}
	close(newfd);
	close(fd);	
	return 0;
}

TCP多进程并发

服务端

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <sys/wait.h>
 
#define BACKLOG 5
void SigHandle(int sig){
    if(sig == SIGCHLD){
        printf("client exited\n");
        wait(NULL);
    }
}
void ClinetHandle(int newfd);
int main(int argc, char *argv[])
{
    int fd, newfd;
    struct sockaddr_in addr, clint_addr;
    socklen_t addrlen = sizeof(clint_addr);
 
#if 0
    struct sigaction act;
    act.sa_handler = SigHandle;
    act.sa_flags = SA_RESTART;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD, &act, NULL);
#else
    signal(SIGCHLD, SigHandle);
#endif
 
    pid_t pid;
     
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>\n", argv[0]);
        exit(0);
    }
 
    /*创建套接字*/
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0){
        perror("socket");
        exit(0);
    }
    addr.sin_family = AF_INET;
    addr.sin_port = htons( atoi(argv[2]) );
    if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
 
    /*地址快速重用*/
    int flag=1,len= sizeof (int); 
    if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { 
              perror("setsockopt"); 
                    exit(1); 
    } 
    /*绑定通信结构体*/
    if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
        perror("bind");
        exit(0);
    }
    /*设置套接字为监听模式*/
    if(listen(fd, BACKLOG) == -1){
        perror("listen");
        exit(0);
    }
    while(1){
        /*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
        newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
        if(newfd < 0){
            perror("accept");
            exit(0);
        }
        printf("addr:%s port:%d\n", inet_ntoa(clint_addr.sin_addr), ntohs(clint_addr.sin_port) );
        if( (pid = fork() ) < 0){
            perror("fork");
            exit(0);
        }else if(pid == 0){
            close(fd);
            ClinetHandle(newfd);
            exit(0);
        }
        else
            close(newfd);
    }
    close(fd);
    return 0;
}
void ClinetHandle(int newfd){
    int ret;
    char buf[BUFSIZ] = {};
    while(1){
        //memset(buf, 0, BUFSIZ);
        bzero(buf, BUFSIZ);
        ret = read(newfd, buf, BUFSIZ);
        if(ret < 0)
        {
            perror("read");
            exit(0);
        }
        else if(ret == 0)
            break;
        else
            printf("buf = %s\n", buf);
    }
    close(newfd);
}

客户端

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
#define BACKLOG 5
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_in addr;
    char buf[BUFSIZ] = {};
 
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>\n", argv[0]);
        exit(0);
    }
 
    /*创建套接字*/
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0){
        perror("socket");
        exit(0);
    }
 
    addr.sin_family = AF_INET;
    addr.sin_port = htons( atoi(argv[2]) );
    if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
 
    /*向服务端发起连接请求*/
    if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
        perror("connect");
        exit(0);
    }
    while(1){
        printf("Input->");
        fgets(buf, BUFSIZ, stdin);
        write(fd, buf, strlen(buf) );
    }
    close(fd);
    return 0}

注意

  • accept函数中后两个参数要做修改

    • 存放服务端接受客户端请求连接后,为其分配的端口号等信息
    • struct sockaddr_in clint_addr
    • socklen_t addrlen
    • 可以用于打印连接的客户端的IP和端口号等信息
  • 在不需要的进程中,关闭不需要的文件描述符fd

  • 僵尸进程的处理

    • 子进程退出后,要对其进行回收,否则占用的系统资源不会回收,变为僵尸进程
    • 利用信号机制SIGCHLD来处理僵尸进程
    • 信号捕捉函数中act.sa_flags成员赋值为SA_RESTART
    • 否则会产生accept: Interrupted system call错误

TCP并发多线程

服务端

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <pthread.h>
 
#define BACKLOG 5
 
void *ClinetHandle(void *arg);
int main(int argc, char *argv[])
{
    int fd, newfd;
    struct sockaddr_in addr, clint_addr;
    pthread_t tid;
    socklen_t addrlen = sizeof(clint_addr);
     
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>\n", argv[0]);
        exit(0);
    }
 
    /*创建套接字*/
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0){
        perror("socket");
        exit(0);
    }
    addr.sin_family = AF_INET;
    addr.sin_port = htons( atoi(argv[2]) );
    if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
 
    /*地址快速重用*/
    int flag=1,len= sizeof (int); 
    if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { 
              perror("setsockopt"); 
                    exit(1); 
    } 
    /*绑定通信结构体*/
    if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
        perror("bind");
        exit(0);
    }
    /*设置套接字为监听模式*/
    if(listen(fd, BACKLOG) == -1){
        perror("listen");
        exit(0);
    }
    while(1){
        /*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
        newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
        if(newfd < 0){
            perror("accept");
            exit(0);
        }
        printf("addr:%s port:%d\n", inet_ntoa(clint_addr.sin_addr), ntohs(clint_addr.sin_port) );
        pthread_create(&tid, NULL, ClinetHandle, &newfd);
        pthread_detach(tid);
    }
    close(fd);
    return 0;
}
void *ClinetHandle(void *arg){
    int ret;
    char buf[BUFSIZ] = {};
    int newfd = *(int *)arg;
    while(1){
        //memset(buf, 0, BUFSIZ);
        bzero(buf, BUFSIZ);
        ret = read(newfd, buf, BUFSIZ);
        if(ret < 0)
        {
            perror("read");
            exit(0);
        }
        else if(ret == 0)
            break;
        else
            printf("buf = %s\n", buf);
    }
    printf("client exited\n");
    close(newfd);
    return NULL;
}

客户端

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
#define BACKLOG 5
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_in addr;
    char buf[BUFSIZ] = {};
 
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>\n", argv[0]);
        exit(0);
    }
 
    /*创建套接字*/
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0){
        perror("socket");
        exit(0);
    }
 
    addr.sin_family = AF_INET;
    addr.sin_port = htons( atoi(argv[2]) );
    if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
 
    /*向服务端发起连接请求*/
    if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
        perror("connect");
        exit(0);
    }
    while(1){
        printf("Input->");
        fgets(buf, BUFSIZ, stdin);
        write(fd, buf, strlen(buf) );
    }
    close(fd);
    return 0;
}

socket接口函数扩展

send与recv函数

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);//接收数据

前三个参数同read/write一样

常见flags:

  • 一般设置为0
  • MSG_PEEK:窥视传入的数据。数据被复制到缓冲区中,但不会从输入队列中删除
  • MSG_OOB:处理带外(OOB)数据

sendto与recvfrom函数

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);
  • 前四个参数同recv/send一样;
  • 后两个参数是通信结构体和结构体的宽度;

UDP通信的实现过程

在这里插入图片描述

UDP通信

服务端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_in addr;
    char buf[BUFSIZ] = {};
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    /*创建套接字*/
    if( (fd = socket(AF_INET, SOCK_DGRAM, 0) ) < 0){
        perror("socket");
        exit(EXIT_FAILURE);
    }
    /*设置通信结构体*/
    bzero(&addr, sizeof(addr) );
    addr.sin_port = htons( atoi(argv[2]) );
    if(inet_aton(argv[1], &addr.sin_addr) == 0) {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
    /*绑定通信结构体*/
    if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
        perror("bind");
        exit(EXIT_FAILURE);
    }
    while(1){
        bzero(buf, BUFSIZ);
        recvfrom(fd, buf, BUFSIZ, 0, NULL, NULL);
        printf("buf=%s\n", buf);
    }
    close(fd);
    return 0;
}

客户端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_in addr;
    char buf[BUFSIZ] = {};
    socklen_t addrlen = sizeof(addr);
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    /*创建套接字*/
    if( (fd = socket(AF_INET, SOCK_DGRAM, 0) ) < 0){
        perror("socket");
        exit(EXIT_FAILURE);
    }
    /*设置通信结构体*/
    bzero(&addr, sizeof(addr) );
    addr.sin_port = htons( atoi(argv[2]) );
    if(inet_aton(argv[1], &addr.sin_addr) == 0) {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
    while(1){
        bzero(buf, BUFSIZ);
        printf("Input->");
        fgets(buf, BUFSIZ, stdin);
        sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&addr, addrlen);
    }
    close(fd);
    return 0;
}

TCP的可靠传输的

TCP的主要特点

TCP 是面向连接的运输层协议,在无连接的、不可靠的 IP网络服务基础之上提供可靠交付的服务。为此,在 IP 的数据报服务基础之上,增加了保证可靠性的一系列措施。

  • TCP 是面向连接的运输层协议。
    • 每一条 TCP 连接只能有两个端点 (endpoint),每一条TCP 连接只能是点对点的(一对一)。
  • TCP 提供可靠交付的服务。
  • TCP 提供全双工通信。
  • 面向字节流
    • TCP 中的“流”(stream) 指的是流入或流出进程的字节序列。
    • 面向字节流:虽然应用程序和 TCP 的交互是一次一个数据块,但 TCP 把应用程序交下来的数据看成仅仅是一连串无结构的字节流。

在这里插入图片描述

在这里插入图片描述

TCP的连接

  • TCP把连接作为最基本的抽象;
  • TCP连接的端点:套接字

在这里插入图片描述

TCP的可靠传输

  • 每发送完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组。
  • 全双工通信的双方既是发送方也是接收方。
  • 假设仅考虑 A 发送数据,而 B 接收数据并发送确认。因此 A 叫做发送方,而 B叫做接收方。

超时重传

  • 为每一个已发送的分组设置一个超时计时器。
  • A 只要在超时计时器到期之前收到了相应的确认,就撤销该超时计时器,继续发送下一个分组 M2 。
  • 若 A 在超时计时器规定时间内没有收到 B 的确认,就认为分组错误或丢失,就重发该分组。

在这里插入图片描述

确认丢失

  • 若 B 所发送的对 M1 的确认丢失了,那么 A 在设定的超时重传时间内将不会收到确认,因此 A 在超时计时器到期后重传 M1。
  • 假定 B 正确收到了 A 重传的分组 M1。这时 B 应采取两个行动:
    • 丢弃这个重复的分组 M1,不向上层交付。
    • 向 A 发送确认。

在这里插入图片描述

确认迟到

  • B 对分组 M1 的确认迟到了,因此 A 在超时计时器到期后重传 M1。
  • B 会收到重复的 M1,丢弃重复的 M1,并重传确认分组。
  • A 会收到重复的确认。对重复的确认的处理:丢弃。

提高传输效率

流水线传输

在这里插入图片描述

连续ARQ协议

  • 发送窗口:发送方维持一个发送窗口,位于发送窗口内的分组都可被连续发送出去,而不需要等待对方的确认。
  • 发送窗口滑动:发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。
  • 累积确认:接收方对按序到达的最后一个分组发送确认,表示:到这个分组为止的所有分组都已正确收到了。

发送窗口

在这里插入图片描述

累积确认

在这里插入图片描述

TCP包头

在这里插入图片描述

  • 源端口就是指本地端口;目的端口就是远程端口

  • 序号是一个随机数seq

    • 若数据中第一个字节的编号是200,数据包中有100个字节
  • 确认号ack

    • 序号200+100=300
    • 确认号为301
  • 数据偏移

    • 确认TCP数据部分
  • FLAG代表6个功能

    • URG紧急指针使能
    • ACK确认号使能
    • PSH数据传输结束,可以交给应用进程;应用进程需要这个数据
    • RST通信过程中数据出现严重问题,连接中断
    • SYN连接管理,发起一个连接请求,SYN置1
    • FIN断开连接,FIN置1
  • 窗口代表窗口的大小

  • 检验和检验TCP数据报的正确性

  • 紧急指针代表着紧急数据结束的地方

  • 选项有多个选项,比如长度可变等

连接的建立

  • TCP建立连接的过程叫做握手
  • 采用三报文握手:在客户和服务器之间交换三个TCP报文段,以防止已失效的连接请求报文段突然又传送到了,因而产生TCP连接建立错误;

TCP的连接管理

三次握手

  • TCP建立连接的过程叫做握手
  • 采用三报文握手:在客户和服务器之间交换三个TCP报文段,以防止已失效的连接请求报文段突然又传送到了,因而产生TCP连接错误建立错误

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

四次挥手

  • TCP 连接释放过程比较复杂。
  • 数据传输结束后,通信的双方都可释放连接。
  • TCP 连接释放过程是四报文握手。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

保活计时器

  • 用来防止在 TCP 连接出现长时期空闲。
  • 通常设置为 2 小时 。
  • 若服务器过了 2 小时还没有收到客户的信息,它就发送探测报文段。
  • 若发送了 10 个探测报文段(每一个相隔 75秒)还没有响

UDP协议

特点

  • 无连接。发送数据之前不需要建立连接。
  • 使用尽最大努力交付。即不保证可靠交付。
  • 面向报文。UDP 一次传送和交付一个完整的报文。
  • 没有拥塞控制。网络出现的拥塞不会使源主机的发送速率降低。很适合多媒体通信的要求。
  • 支持一对一、一对多、多对一、多对多等交互通信。
  • 首部开销小,只有 8 个字节。

UDP 通信的特点:简单方便,但不可靠。

UDP是面向报文的

在这里插入图片描述

UDP的数据格式

在这里插入图片描述

17:UDP协议编号

检验和:检验数据正确性

IP协议

IP数据报的格式

在这里插入图片描述

版本:IPv4、IPv6

首部长度

  • 长度=4×该部分的数值

  • 占 4 位,可表示的最大数值是 15 个单位(一个单位为 4 字节),因此 IP 的首部长度的最大值是 60 字节

区分服务

  • 占 8 位,用来获得更好的服务。只有在使用区分服务(比如传输优先级)时,这个字段才起作用
  • 在一般的情况下都不使用这个字段

总长度

  • 指首部和数据之和的长度

  • 单位为字节,因此数据报的最大长度为 65535 字节

  • 总长度必须不超过最大传送单元 MTU

标识

  • 它是一个计数器,用来产生 IP 数据报的标识,是一个随机数

标志

  • 占 3 位,目前只有前两位有意义

  • 标志字段的最低位是 MF

    • MF=1 表示后面还有分片
    • MF=0 表示最后一个分片
  • 标志字段中间的一位是 DF (Don’t Fragment) 只有当 DF=0 时才允许分片。

片偏移

  • 占 13 位,指出:较长的分组在分片后某片在原分组中的相对位置
  • 片偏移以8个字节为偏移单位。

生存时间

  • 记为 TTL (Time To Live),指示数据报在网络中可通过的路由器数的最大值

协议

  • TCP、UDP

首部检验和

  • 检测数据正确性
  • 两个字节一组检测,只检验首部

源地址来自那里

目的地址去哪里

IP数据报分片

MTU:IP数据报的最大长度

在这里插入图片描述

以太网协议

数据链路层信道类型

  • 点对点信道
    • 使用一对一的点对点通信方式。
  • 广播信道
    • 使用一对多的广播通信方式。
    • 必须使用专用的共享信道协议来协调这些主机的数据发送。

以太网 V2 的 MAC 帧格式

在这里插入图片描述

目的地址:目的MAC地址

源地址:源MAC地址

类型:那个协议的数据

  • ARP协议(链路层和网络层中介的协议及MAC地址和IP地址直接的协议)
  • IP协议
  • 类型字段用来标志上一层使用的是什么协议,以便把收到的 MAC 帧的数据上交给上一层的这个协议

数据:IP数据报

  • 数据字段的正式名称是 MAC 客户数据字段
  • 最小长度 64 字节 - 18 字节的首部和尾部 = 数据字段的最小长度(46字节)

FCS

  • CRC算法 检测每个比特位的正确性
  • CRC得出来的结果 及要填充的就是FCS

无效的MAC帧

  • 数据字段的长度与长度字段的值不一致

  • 帧的长度不是整数个字节

  • 用收到的帧检验序列 FCS 查出有差错

  • 数据字段的长度不在 46 ~ 1500 字节之间

  • 有效的 MAC 帧长度为 64 ~ 1518 字节之间

  • 对于检查出的无效 MAC 帧就简单地丢弃

  • 以太网不负责重传丢弃的帧

UNIX域套接字理论

本地地址

struct sockaddr_un
{
  unsigned short sun_family;/* 协议类型 */
	char sun_path[108];/* 套接字文件路径 */
};

UNIX 域流式套接字

UNIX 域流式套接字的用法和 TCP 套接字基本一致,区别在于使用的协议和地址不同

UNIX 域流式套接字服务器端流程如下:

  • 创建 UNIX 域流式套接字
  • 绑定本地地址(套接字文件)
  • 设置监听模式
  • 接收客户端的连接请求
  • 发送/接收数据

UNIX 域流式套接字客户端流程如下

  • 创建 UNIX 域流式套接字
  • 指定服务器端地址(套接字文件)
  • 建立连接
  • 发送/接收数据

UNIX域流式套接字

服务端

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define LISTEN_BACKLOG 50
 
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
int main(int argc, char *argv[])
{
    int sfd, cfd;
    struct sockaddr_un my_addr, peer_addr;
    socklen_t peer_addr_size;
    char buf[BUFSIZ] = {};
 
    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1)
        handle_error("socket");
 
    memset(&my_addr, 0, sizeof(struct sockaddr_un));
    my_addr.sun_family = AF_UNIX;
    strncpy(my_addr.sun_path, MY_SOCK_PATH,
            sizeof(my_addr.sun_path) - 1);
 
    if (bind(sfd, (struct sockaddr *) &my_addr,
                sizeof(struct sockaddr_un)) == -1)
        handle_error("bind");
    if (listen(sfd, LISTEN_BACKLOG) == -1)
        handle_error("listen");
 
    peer_addr_size = sizeof(struct sockaddr_un);
    cfd = accept(sfd, (struct sockaddr *) &peer_addr,
            &peer_addr_size);
    if (cfd == -1)
        handle_error("accept");
     
    recv(cfd, buf, BUFSIZ, 0);
    printf("%s\n", buf);
 
    close(cfd);
    close(sfd);
    remove(MY_SOCK_PATH);
    return 0;
}

客户端

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
#define MY_SOCK_PATH "/tmp/my_sock_file"
 
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_un peer_addr;
    char buf[BUFSIZ] = {"Hello World!"};
 
    fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd == -1)
        handle_error("socket");
 
    memset(&peer_addr, 0, sizeof(struct sockaddr_un));
    peer_addr.sun_family = AF_UNIX;
    strncpy(peer_addr.sun_path, MY_SOCK_PATH,
            sizeof(peer_addr.sun_path) - 1);
 
    if (connect(fd, (struct sockaddr *) &peer_addr,
                sizeof(struct sockaddr_un)) == -1)
        handle_error("connect");
 
    printf("%s\n",buf);
    send(fd, buf, strlen(buf), 0);
 
    close(fd);
    return 0;
}

UNIX数据报套接字

服务端

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_un my_addr, peer_addr;
    socklen_t peer_addr_size;
    char buf[BUFSIZ] = {};
 
    fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (fd == -1)
        handle_error("socket");
 
    memset(&my_addr, 0, sizeof(struct sockaddr_un));
    my_addr.sun_family = AF_UNIX;
    strncpy(my_addr.sun_path, MY_SOCK_PATH,
            sizeof(my_addr.sun_path) - 1);
 
    if (bind(fd, (struct sockaddr *) &my_addr,
                sizeof(struct sockaddr_un)) == -1)
        handle_error("bind");
 
    peer_addr_size = sizeof(struct sockaddr_un);
    recvfrom(fd, buf, BUFSIZ, 0, (struct sockaddr *) &peer_addr,
            &peer_addr_size);
    printf("%s\n",buf);
    close(fd);
    remove(MY_SOCK_PATH);
    return 0;
}

客户端

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_un peer_addr;
    socklen_t peer_addr_size;
    char buf[BUFSIZ] = {"Hello World!"};
 
    fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (fd == -1)
        handle_error("socket");
 
    memset(&peer_addr, 0, sizeof(struct sockaddr_un));
    peer_addr.sun_family = AF_UNIX;
    strncpy(peer_addr.sun_path, MY_SOCK_PATH,
            sizeof(peer_addr.sun_path) - 1);
 
    peer_addr_size = sizeof(struct sockaddr_un);
    printf("%s\n", buf);
    sendto(fd, buf, strlen(buf), 0, (struct sockaddr *) &peer_addr,
            peer_addr_size);
    close(fd);
    remove(MY_SOCK_PATH);
    return 0;
}

IO模型

基本概念

IO的基本概念

  • I/O即数据的读取(接收)或写入(发送)操作

  • 通常用户进程中的一个完整I/O分为两个阶段

    • 用户进程空间<—>内核空间
    • 内核空间<—>设备空间(磁盘、网卡等)
  • I/O分为内存I/O、网络I/O和磁盘I/O三种

同步和异步

  • 对于一个线程的请求调用来讲,同步和异步的区别在于是否要等这个请求出最终结果
  • 对于多个线程而言,同步或异步就是线程间的步调是否要一致、是否要协调
  • 同步也经常用在一个线程内先后两个函数的调用上
  • 异步就是一个请求返回时一定不知道结果,还得通过其他机制来获知结果,如:主动轮询或被动通知

阻塞和非阻塞

  • 阻塞与非阻塞与等待消息通知时的状态(调用线程)有关
  • 阻塞和同步是完全不同的概念。同步是对于消息的通知机制而言,阻塞是针对等待消息通知时的状态来说的
  • 进程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态

进入阻塞的原因

  • 线程通过调用sleep方式进休眠状态
  • 线程调用一个在I/O上被阻塞的操作,即该操作在输入/输出操作完成前不会返回到它的调用者
  • 线程试图得到一个锁,而该锁正被其他线程持有,于是只能进入阻塞状态,等到获取了同步锁,才能恢复执行
  • 线程在等待某个触发条件

可能阻塞套接字的Linux Sockets API调用分为以下四种

  • 输入操作
  • 输出操作
  • 接受连接
  • 外出连接

五种IO模型

同步IO

  • 阻塞IO
  • 非阻塞IO
  • 多路复用IO
  • 信号驱动式IO

异步IO

阻塞IO模型

非阻塞IO

在这里插入图片描述

多路复用IO

在这里插入图片描述

第一阶段为阻塞或非阻塞

第二阶段为阻塞(同步)

select函数可以监听多个文件描述符,内核中的无数据报准备好,可能是所有文件描述符都没有准备好,当有一个或几个文件描述符准备好的时候,返回可读条件,告诉哪一个文件描述符有数据或没有数据,当有数据时,调用recvfrom函数拷贝数据报,返回成功指示,处理数据报

信号驱动式IO

在这里插入图片描述

第一阶段为异步

第二阶段为同步,阻塞

异步IO

在这里插入图片描述

全程为非阻塞方式

aio_read函数发起一个读的请求,告诉内核需要哪一个文件描述符的数据后结束调用,内核在等待数据报准备好之后将数据包拷贝到用户空间,拷贝完成;应用进程在内核处理期间,该干什么干什么,全程为非阻塞;发送一个信号,信号处理函数处理数据报

五种I/O模型比较

在这里插入图片描述

IO多路复用的实现方式

select函数

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

参数:

  • nfds 是三个集合中编号最高的文件描述符,加上1

  • eadfds 可读集合

  • writefds 可写集合

  • exceptfds 异常集合

  • timeout

    • NULL 永久阻塞
    • 0 非阻塞模式
  • struct timeval结构体

struct timeval{
  long tv_sec;   /*秒*/
  long tv_usec;  /*微秒*/
}
  • fd_set结构体

在这里插入图片描述

/*将文件描述符从集合中删除*/
void FD_CLR(int fd, fd_set *set);

/*查看文件描述符是否存在于集合当中*/
int FD_ISSET(int fd, fd_set *set)/*添加文件描述符*/
void FD_SET(int fd, fd_set *set);

/*初始化集合*/
void FD_ZERO(fd_set *set);

多路复用select代码实现

net.h

#ifndef __NET_H__
#define __NET_H__

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

#define MAX_SOCK_FD 1024

struct sockaddr_in newaddr;
struct sockaddr_in addr;
#define BACKLOG 5
#define handle_error(msg) \
           do { perror(msg); exit(EXIT_FAILURE); } while (0)

void Argment(int argc,char *argv[]);
int Create_socket(char *argv[]);
int Data_handle(int newfd);


#endif


socket.c

#include "net.h"

void Argment(int argc,char *argv[])
{
	if(argc < 3)
	{
		fprintf(stderr,"%s<addr><post>",argv[0]);
		exit(0);
	}
}

int Create_socket(char *argv[])
{	
	int fd,ret;
	/*创建套接字*/
	fd = socket(AF_INET, SOCK_STREAM,0);
	if(fd < 0)
		handle_error("socket");
	/*允许地址快速重用*/
	int flag = 1;
	if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag)))
		perror("setsockopt");
	/*绑定通信结构体*/
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	inet_aton(argv[1], &addr.sin_addr);

	if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)))
		handle_error("bind");
	if(listen(fd,BACKLOG))
		handle_error("listen");
	return fd;
	
}

int Data_handle(int newfd)
{
	int ret;
	char buf[BUFSIZ]={};
    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(struct sockaddr_in);
    if(getpeername(newfd, (struct sockaddr *)&peeraddr, &peerlen))
        perror("getpeername");
    ret = recv(newfd,buf,BUFSIZ,0);
    if(ret < 0)
        perror("recv");
    if(ret > 0){
        printf("[%s:%d]data: %s\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port),buf);
    }
	return ret;
}


server.c

#include "net.h"
#include <sys/select.h>
#define MAX_SOCK_FD 1024
 
int main(int argc, char *argv[])
{
    int i, ret, fd, newfd;
    fd_set set, tmpset;
    Addr_in clientaddr;
    socklen_t clientlen = sizeof(Addr_in);
    /*检查参数,小于3个 直接退出进程*/
    Argment(argc, argv);
    /*创建已设置监听模式的套接字*/
    fd = CreateSocket(argv);
 
    FD_ZERO(&set);
    FD_ZERO(&tmpset);
    FD_SET(fd, &set);
    while(1){
        tmpset = set;
        if( (ret = select(MAX_SOCK_FD, &tmpset, NULL, NULL, NULL)) < 0)
            ErrExit("select");
        if(FD_ISSET(fd, &tmpset) ){
            /*接收客户端连接,并生成新的文件描述符*/
            if( (newfd = accept(fd, (Addr *)&clientaddr, &clientlen) ) < 0)
                perror("accept");
            printf("[%s:%d]已建立连接\n", 
                    inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
            FD_SET(newfd, &set);
        }else{ //处理客户端数据
            for(i = fd + 1; i < MAX_SOCK_FD; i++){
                if(FD_ISSET(i, &tmpset)){
                    if( DataHandle(i) <= 0){
                        if( getpeername(i, (Addr *)&clientaddr, &clientlen) )
                            perror("getpeername");
                        printf("[%s:%d]断开连接\n", 
                                inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
                        FD_CLR(i, &set);
                    }
                }
            }
        }
    }
    return 0;
}

poll函数

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
struct pollfd
{
  int fd;/*文件描述符*/
  short events;/*请求的事件*/
  short revents;/*返回的事件 由系统填充*/
}

参数:

events 事件类型

  • POLLIN 有数据可读
  • POLLPRI 有紧急数据需要读取
  • POLLOUT 文件可写

nfds文件描述符的数量

timeout时间单位毫秒

  • 设置阻塞的时间
  • 0为非阻塞
  • 负数表示永久阻塞

多路复用poll代码实现

#include "net.h"

int main(int argc,char* argv[])
{
	int i, j, ret, fd, newfd;
	nfds_t nfds = 1;
	struct pollfd fds[MAX_SOCK_FD] = {};
	socklen_t clientlen = sizeof(struct sockaddr_in );
	/*检查参数,小于3个 直接退出进程*/
	Argment(argc, argv);
	/*创建已设置监听模式的套接字*/
	fd = Create_socket(argv);

	fds[0].fd = fd;
	fds[0].events = POLLIN;
	
	while(1){
		if( poll(fds,nfds,-1) < 0)
			handle_error("poll");
		for(i = 0;i < nfds;i++)//轮询文件描述符 找到有数据可读的文件描述符
		{
			if(fds[i].fd == fd && fds[i].revents & POLLIN)//如果文件描述符和fd相同,则是建立连接请求
			{
				/*接收客户端连接,并生成新的文件描述符*/
				if( (newfd = accept(fd, (struct sockaddr  *)&newaddr, &clientlen) ) < 0)
					perror("accept");
				printf("[%s:%d]已建立连接\n", 	inet_ntoa(newaddr.sin_addr), ntohs(newaddr.sin_port));
				fds[nfds].fd = newfd;
				fds[nfds++].events = POLLIN;
			}
			if(i > 0 && fds[i].revents & POLLIN)//如果文件描述符不和fd相等,则是其他已经建立连接的文件描述符,它的数据处理请求
			{
				/*处理客户端数据*/
				if( Data_handle(fds[i].fd) <= 0)
				{
					if( getpeername(fds[i].fd, (struct sockaddr *)&newaddr, &clientlen) )
						perror("getpeername");
					printf("[%s:%d]断开连接\n", inet_ntoa(newaddr.sin_addr), ntohs(newaddr.sin_port));
					close(fds[i].fd);
					for (j = i; j < nfds+1; ++j)
					{
						fds[j] = fds[j+1];	
					}
					nfds--;
					i--;
				}
			}
		}
	}
	return 0;
}

epoll函数族

/*创建epoll句柄*/
int epoll_create(int size);//size参数实际上已将其弃用
/*epoll句柄的控制接口*/
int epoll_ctl(int epfd,int op,int fd,struct epoll_event * event);
/*等待epoll文件描述符上的IO事件*/
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

typedef union epoll_data
{
  void *ptr;
  int fd;
  __uint32_t u32;
  __uint64_t u64;
}epoll_data_t;

struct epoll_event
{
  __uint32_t events;/* Epoll events */
  epoll_data_t data;/* User data variable */
};

参数

epfd

  • epoll专用的文件描述符,epoll_create()的返回值

op表示动作,用三个宏来表示

  • EPOLL_CTL_ADD 注册新的fd到epfd中
  • EPOLL_CTL_MOD 修改已经注册的fd的监听事件
  • EPOLL_CTL_DEL 从epfd中删除一个fd

fd

  • 需要监听的文件描述符

event

  • 告诉内核要监听什么事件
参数释义
EPOLLIN表示对应的文件描述符可以读(包括对端 SOCKET正常关闭)
EPOLLOUT表示对应的文件描述符可以写;
EPOLLPRI表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR表示对应的文件描述符发生错误
EPOLLHUP表示对应的文件描述符被挂断
EPOLLET将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的
EPOLLONESHOT只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个socket 加入到 EPOLL 队列里

events

  • 分配好的 epoll_event 结构体数组
  • epoll_wait 将会把发生的事件赋值到events 数组中

maxevents

  • events 数组的元素个数

timeout

  • 超时时间,单位为毫秒
  • 为-1 时,函数为阻塞

epoll函数实现

#include "net.h"

int main(int argc,char* argv[])
{
	int i, nfds, fd, epfd, newfd;
	socklen_t clientlen = sizeof(struct sockaddr_in );
	struct epoll_event tmp,events[MAX_SOCK_FD] = {};//tmp缓存新的事件属性 events缓存发生时间的文件描述符fd
	/*检查参数,小于3个 直接退出进程*/
	Argment(argc, argv);
	/*创建已设置监听模式的套接字*/
	fd = Create_socket(argv);

	if((epfd = epoll_create(1)) < 0)//创建epoll的句柄epfd
		handle_error("epoll_create");
	tmp.events = EPOLLIN;//设置监听事件的属性
	tmp.data.fd = fd;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&tmp))//将fd添加到epfd中监听
		handle_error("epoll_ctl");
	
	while(1)
	{
		if(( nfds = epoll_wait(epfd,events,MAX_SOCK_FD,-1)) < 0)//监听epfd中所存fd对应的事件 nfds发生事件的文件描述符的个数
			handle_error("epoll_wait");
		printf("nfds = %d\n", nfds);
		for(i = 0;i < nfds;i++)//循环处理发生事件的文件描述符
		{
			if(events[i].data.fd == fd)
//如果文件描述符等于fd 则是发生连接请求
			{
				/*接收客户端连接,并生成新的文件描述符*/
				if( (newfd = accept(fd, (struct sockaddr*)&newaddr, &clientlen) ) < 0)
					perror("accept");
				printf("[%s:%d]已建立连接\n", 	inet_ntoa(newaddr.sin_addr), ntohs(newaddr.sin_port));
				tmp.events = EPOLLIN;//将其注册到epfd中
                tmp.data.fd = newfd;
				if(epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&tmp))
                    handle_error("epoll_ctl");
			}
			else
			{
				/*处理客户端数据*/
				if( Data_handle(events[i].data.fd) <= 0)//如果Data_handle返回值小于0 则终止连接
				{
					if( epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL) )//将终止连接的文件描述符删除
						handle_error("epoll_ctl");
					if( getpeername(events[i].data.fd, (struct sockaddr *)&newaddr, &clientlen) )
						perror("getpeername");
					printf("[%s:%d]断开连接\n", inet_ntoa(newaddr.sin_addr), ntohs(newaddr.sin_port));
					close(fds[i].fd);
				}
			}
		}
	}
	close(epfd);
    close(fd);
	return 0;
}

select,poll和epoll各自优缺点

select
  • 单个进程能够监视的文件描述符的数量有最大限制,通常是1024,虽然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差

  • 内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,会产生巨大的开销

  • select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件

  • select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行I/O操作,那么之后每次select调用还是会将这些文件描述符通知进程

  • 内核中实现select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄

假设服务器需要支持100万的并发连接,在__FD_SETSIZE1024的情况下,则我们至少需要开辟1000个进程才能实现100万的并发连接

poll

poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在 select与poll目前在小规模服务器上还是有用武之地,并且维护老系统代码的时候,经常会用到这两个函数;

epoll

epoll是Linux下多路复用I/O接口select/poll的增强版本 epoll只需要监听那些已经准备好的队列集合中的文件描述符,效率较高

在这里插入图片描述

在这里插入图片描述

套接字属性设置

基本概念

  • 设置套接字的选项对套接字进行控制
  • 除了设置选项外,还可以获取选项
  • 选项的概念相当于属性,所以套接字选项也可说是套接字属性
  • 有些选项(属性)只可获取,不可设置
  • 有些选项既可设置也可获取

选项级别

选项释义
SOL_SOCKET该级别的选项只作用于套接字本身
SOL_LRLMP该级别的选项作用于IrDA协议
IPPROTO_IP该级别的选项作用于IPv4协议
IPPROTO_IPV6该级别的选项作用于IPv6协议
IPPROTO_RM该级别的选项作用于可靠的多播传输
IPPROTO_TCP该级别的选项适用于流式套接字
IPPROTO_UDP该级别的选项适用于数据报套接字

SOL_SOCKET的常用选项

选项名称说明获取/设置
SO_ACCEPTCONN套接字是否处于监听状态获取
SO_BROADCAST允许发送广播数据两者都可
SO_DEBUG允许调试两者都可
SO_DONTROUTE不查找路由两者都可
SO_ERROR获得套接字错误获取
SO_KEEPALIVE保活连接两者都可
SO_LINGER延迟关闭连接两者都可
SO_OOBINLINE带外数据放入正常数据流两者都可
SO_RCVBUF接收缓冲区大小两者都可
SO_SNDBUF发送缓冲区大小两者都可
SO_REUSEADDR允许重用本地地址和端口两者都可
SO_TYPE获得套接字类型获取

IPPROTO_IP级别的常用选项

选项名称说明获取/设置
IP_ADD_MEMBERSHIP加入一个多播组设置
IP_OPTIONS获取或设置IP头部内的选项两者都可
IP_HDRINCL用户在用户数据前面提供 IP 标头(用于原始套接字)两者都可
IP_TTLIP TTL相关两者都可

getsockopt获取套接字选项

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

参数:

  • sockfd 套接字描述符
  • level 表示选项的级别
  • optname 表示要获取的选项名称
  • optval 指向存放接收到的选项内容的缓冲区
  • optlen 指向optval所指缓冲区的大小

函数返回值:

  • 执行成功返回0
  • 失败返回‒1,errno来获取错误码
常见的错误码:
  • EBADF:参数sockfd不是有效的文件描述符
  • EFAULT:参数optlen太小或optval所指缓冲区非法
  • EINVAL:参数level未知或非法
  • ENOPROTOOPT:选项未知或不被指定的协议族所支持
  • ENOTSOCK:描述符不是一个套接字描述符

示例

获取流套接字和数据报套接字接收和发送的(内核)缓冲区大小

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
 
int main()
{
	int err,s = socket(AF_INET, SOCK_STREAM, 0);//创建流套接字TCP
	if (s == -1) {
		printf("Error at socket()\n");
		return -1;
	}
	int su = socket(AF_INET, SOCK_DGRAM, 0); //创建数据报套接字UDP
	if (s == -1) {
		printf("Error at socket()\n");
		return -1;
	}

	int optVal;
	int optLen = sizeof(optVal);
	//获取流套接字接收缓冲区大小
	if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char*)&optVal,
                   (socklen_t *)&optLen) == -1)
		printf("getsockopt failed:%d", errno);
	else
		printf("Size of stream socket receive buffer: %ld bytes\n", optVal);
	//获取流套接字发送缓冲区大小
	if (getsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)&optVal,
                   (socklen_t *)&optLen) == -1)
		printf("getsockopt failed:%d", errno);
	else 
		printf("Size of streaming socket send buffer: %ld bytes\n", optVal);

	//获取数据报套接字接收缓冲区大小
	if (getsockopt(su, SOL_SOCKET, SO_RCVBUF, (char*)&optVal,
                   (socklen_t *)&optLen) == -1)
		printf("getsockopt failed:%d", errno);
	else
		printf("Size of datagram socket receive buffer: %ld bytes\n", optVal);
	//获取数据报套接字发送缓冲区大小
	if (getsockopt(su, SOL_SOCKET, SO_SNDBUF, (char*)&optVal,
                   (socklen_t *)&optLen) == -1)
		printf("getsockopt failed:%d", errno);
	else
		printf("Size of datagram socket send buffer:%ld bytes\n", optVal);

	getchar();
	return 0;
}

示例:判断套接字是否处于监听状态

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/time.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
 
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;

#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char *argv[])
{
        Addr_in service;
        if(argc < 3)
        {
                printf("%s[ADDR][PORT]\n", argv[0]);
                exit(0);
        }
        int s = socket(AF_INET, SOCK_STREAM, 0); //创建一个流套接字
        if (s == -1) 
                ErrExit("socket");
        //允许地址的立即重用
        char on = 1;
        setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

        service.sin_family = AF_INET;
        service.sin_addr.s_addr = inet_addr(argv[1]);
        service.sin_port = htons( atoi(argv[2]) );
        if (bind(s, (Addr*)&service, sizeof(service)) == -1) //绑定套接字
                ErrExit("bind");
        int optVal;
        int optLen = sizeof(optVal);
        //获取选项SO_ACCEPTCONN的值
        if (getsockopt(s, SOL_SOCKET, SO_ACCEPTCONN, (char*)&optVal, (socklen_t*)&optLen) == -1)
                printf("getsockopt failed:%d",errno);
        else printf("Before listening, The value of SO_ACCEPTCONN:%d, The socket is not listening\n", optVal);

        // 开始侦听
        if (listen(s, 100) == -1)
                ErrExit("listen");
        //获取选项SO_ACCEPTCONN的值
        if (getsockopt(s, SOL_SOCKET, SO_ACCEPTCONN, (char*)&optVal, (socklen_t*)&optLen) == -1)
                ErrExit("getsockopt");
        else printf("After listening,The value of SO_ACCEPTCONN:%d, The socket is listening\n", optVal);
        return 0;
}

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 表示选项的级别
  • optname 表示要设置的选项名称
  • optval 指向存放接收到的选项内容的缓冲区
  • optlen 指向optval所指缓冲区的大小

函数返回值

  • 执行成功返回0
  • 失败返回‒1,errno来获取错误码

示例

示例:启用套接字的保活机制

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/time.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
 
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;

#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char *argv[])
{
        Addr_in service;
        if(argc < 3)
        {
                printf("%s[ADDR][PORT]\n", argv[0]);
                exit(0);
        }

        int s = socket(AF_INET, SOCK_STREAM, 0); //创建一个流套接字
        if( s < 0)
                ErrExit("socket");

        char on = 1;
        setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

        service.sin_family = AF_INET;
        service.sin_addr.s_addr = inet_addr(argv[1]);
        service.sin_port = htons(atoi(argv[2]));
        if (bind(s, (Addr *) &service, sizeof(service)) == -1) //绑定套接字
                ErrExit("bind");

        int optVal = 1;//一定要初始化
        int optLen = sizeof(int);

        //获取选项SO_KEEPALIVE的值
        if (getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, (socklen_t *)&optLen) == -1)
                ErrExit("getsockopt");
        else printf("After listening,the value of SO_ACCEPTCONN:%d\n", optVal);

        optVal = 1;
        if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, optLen) != -1)
                printf("Successful activation of keep alive mechanism.\n");//启用保活机制成功

        if (getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, (socklen_t *)&optLen) == -1)
                ErrExit("getsockopt");
        else printf("After setting,the value of SO_KEEPALIVE:%d\n", optVal);

        return 0;

示例:使用SOL_TCP字段设置keepalive

SOL_TCP字段详细解释参考

[SO_KEEPALIVE选项]

int keepAlive = 1;//设定KeepAlive
int keepIdle = 5;//开始首次KeepAlive探测前的TCP空闭时间
int keepInterval = 5;//两次KeepAlive探测间的时间间隔
int keepCount = 3;//判定断开前的KeepAlive探测次数
setKeepAlive (newfd, keepAlive, keepIdle, keepInterval,keepCount);

void setKeepAlive (int sockfd, int attr_on, socklen_t idle_time, socklen_t interval, socklen_t cnt)
{
    setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, (const char *) &attr_on, sizeof (attr_on));
    setsockopt (sockfd, SOL_TCP, TCP_KEEPIDLE, (const char *) &idle_time, sizeof (idle_time));
    setsockopt (sockfd, SOL_TCP, TCP_KEEPINTVL, (const char *) &interval, sizeof (interval));
    setsockopt (sockfd, SOL_TCP, TCP_KEEPCNT, (const char *) &cnt, sizeof (cnt));
}

广播和组播

广播

  • 数据包发送方式只有一个接受方,称为单播

  • 如果同时发给局域网中的所有主机,称为广播

  • 只有用户数据报(使用UDP协议)套接字才能广播

  • 广播地址

    • 一个网络内主机号全为1的IP地址为广播地址

    • 发到该地址的数据包被所有的主机接收

    • 255.255.255.255在所有网段中都代表广播地址

广播的实现

在这里插入图片描述

注意:

  • 发送端需要开启套接字发送广播信息的能力

  • int on = 1;
    setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
    

代码实现

发送端

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> 
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
 
#define ErrExit(msg) do {perror(msg); exit(EXIT_FAILURE);} while(0)
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
 
int main(int argc, char *argv[])
{
    int fd = -1;
    Addr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    char buf[BUFSIZ] = {};
    /*参数检查*/
    if(argc < 3){
        fprintf(stderr, "%s<multiaddr><port>", argv[0]);
        exit(EXIT_FAILURE);
    }
    /*创建套接字*/
    if( (fd = socket(AF_INET, SOCK_DGRAM, 0) ) < 0)
        ErrExit("socket");
 
    /*允许广播*/
    int on = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
 
    /*设置通信结构体*/
    peeraddr.sin_family = AF_INET;
    peeraddr.sin_port = htons(atoi(argv[2]));
    if(!inet_aton(argv[1], &peeraddr.sin_addr) ){
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
    while(1){
        fgets(buf, BUFSIZ, stdin);
        sendto(fd, buf, strlen(buf)+1, 0, (Addr *)&peeraddr, peerlen);
    }
    return 0;
}

组播

  • 在 IP 多播数据报的目的地址需要写入多播组的标识符。
  • 多播组的标识符就是 IP 地址中的 D 类地址(多播地址)
    • 地址范围:224.0.0.0 ~ 239.255.255.255
  • 每一个 D 类地址标志一个多播组。
  • 多播地址只能用于目的地址,不能用于源地址

组播的实现

  • 创建用户数据报套接字
  • 加入多播组
  • 绑定组播IP地址和端口
  • 等待接受数据报
struct ip_mreqn
{
  struct in_addr imr_multiaddr; /*IP 组播组地址*/
  struct in_addr imr_address;/*本地接口的IP地址*/
  int imr_ifindex;/*本地网卡的编号*/
}

struct ip_mreq
{
  struct in_addr imr_multiaddr; /*IP 组播组地址*/
  struct in_addr imr_interface; /*本地接口的IP地址*/
}

if(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq)) < 0)
{
  perror("setsockopt");
  exit(0);
}

代码实现

服务端

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
 
#define ErrExit(msg) do {perror(msg); exit(EXIT_FAILURE);} while(0)
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
 
int main(int argc, char *argv[])
{
    int fd = -1;
    Addr_in myaddr, peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    struct ip_mreqn mreq;
    char buf[BUFSIZ] = {};
    /*参数检查*/
    if(argc < 3){
        fprintf(stderr, "%s<addr><port>", argv[0]);
        exit(EXIT_FAILURE);
    }
    /*创建套接字*/
    if( (fd = socket(AF_INET, SOCK_DGRAM, 0) ) < 0)
            ErrExit("socket");
    /*加入多播组*/
    bzero(&mreq, sizeof(mreq) );
    if(!inet_aton(argv[1], &mreq.imr_multiaddr) ){
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
    if(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0){
        perror("setsockopt");
        exit(0);
    }
 
    /*设置通信结构体*/
    myaddr.sin_family = AF_INET;
    myaddr.sin_port = htons(atoi(argv[2]));
    if(!inet_aton(argv[1], &myaddr.sin_addr) ){
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
    /*绑定通信结构体*/
    if( bind(fd, (Addr *)&myaddr, sizeof(Addr_in)) )
        ErrExit("bind");
    while(1){
        recvfrom(fd, buf, BUFSIZ, 0, (Addr *)&peeraddr, &peerlen);
        printf("[%s:%d]%s\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);
    }
    return 0;
}

原始套接字

网络层原始套接字

  • 原始套接字读到的是整个IP数据报,包括IP包头
  • 原始套接字是一种特殊的套接字,它可以让你直接访问网络层和链路层。
  • 在Linux中,原始套接字的创建和TCP、UDP编程一样使用socket函数来实现,只不过使用的协议族、套接字类型和协议类型不同而已。
  • 在原始套接字中,第三个参数不像标准套接字那样设置为0,而是根据具体的协议设置不同的协议类型。例如,如果你想要访问IP层,那么你需要将第三个参数设置为IPPROTO_IP。

原始套接字的创建

sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_TCP)
//头文件<netinet/in.h> 

参数

AF_INET

  • IPv4网络协议的套接字类型

SOCK_RAW

  • 原始套接字

IPPROTO_TCP

  • 指明相应的协议,协议类型的常用取值有
    • IPPROTO_IP IP协议
    • IPPROTO_ICMP ICMP协议
    • IPPROTO_TCP TCP协议
    • IPPROTO_UDP UDP协议
    • IPPROTO_RAW 原始套接字协议
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ether.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <sys/ioctl.h>
 
#define MTU 1500
 
int main()
{
    /* 定义变量 */
    int sockfd = -1, len, datalen, i;
    uint8_t buf[MTU]={}, *data;
 
    struct iphdr *iph;  //IP包头
    struct tcphdr *tcph;//TCP包头
    struct winsize size;
 
    /* 创建一个原始套接字 */
    if( (sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP) ) < 0)
    {
        perror("socket");
        return 0;
    }
    printf("sockfd = %d\n", sockfd);
 
    /* 接收(只接收TCP数据协议)并处理IP数据报 */
    while(1)
    {
        /* 接收包含TCP协议的IP数据报 */
        len = recvfrom(sockfd, buf, sizeof(buf),0,NULL,NULL);
        printf("IP数据报长度 = %d\n", len);
 
        /* 打印源IP和目的IP */
        iph = (struct iphdr *)buf;
        printf("源IP:%s",inet_ntoa(*(struct in_addr *)&iph->saddr) );
        printf("目的IP%s\n",inet_ntoa(*(struct in_addr *)&iph->daddr) );
 
        /* 打印TCP包头的源端口号和目的端口号 */
        tcph = (struct tcphdr *)(buf+iph->ihl*4);
        printf("%hu--->", ntohs(tcph->source));
        printf("%hu\n", ntohs(tcph->dest));
 
        /* 打印TCP数据段的长度 */
        printf("TCP首部长度:%d\n", tcph->doff*4);
        if(iph->ihl*4+tcph->doff*4 < len) {
            data = buf + iph->ihl*4 + tcph->doff*4;
            datalen = len - iph->ihl*4 + tcph->doff*4;
            ioctl(STDIN_FILENO,TIOCGWINSZ,&size); //terminal 结构体
            for(i = 0; i < size.ws_col; i++) //显示一行 = 
                putchar('=');
            putchar('\n');
            printf("TCP数据字符:\n");
            for(i = 0; i < size.ws_col; i++)
                putchar('=');
            putchar('\n');
            for(i = 0; i < datalen-1; i++) {
                printf("%c", data[i]);
            }
            for(i = 0; i < size.ws_col; i++)
                putchar('=');
            putchar('\n');
            printf("TCP数据16进制:\n");
            for(i = 0; i < size.ws_col; i++)
                putchar('=');
            putchar('\n');
            for(i = 0; i < datalen-1; i++){
                printf("%x ", data[i]);
            }
            putchar('\n');
            for(i = 0; i < size.ws_col; i++)
                putchar('=');
            putchar('\n');
        }
    }
    //关闭套接字
    close(sockfd);
    return 0;
}

链路层原始套接字

socked = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL)) < 0);
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ether.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <net/ethernet.h>
 
#define MTU 1500
 
int main()
{
    /* 定义变量 */
    int sockfd, len;
    uint8_t buf[MTU]={};
    uint16_t ether_type;
 
    struct iphdr *iph;  //IP包头
    struct tcphdr *tcph;//TCP包头
    struct ether_header *eth;
 
    /* 创建一个链路层原始套接字 */
    if( (sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) ) < 0){
        perror("socket");
        return 0;
    }
    printf("sockfd = %d\n", sockfd);
 
    /* 接收(只接收TCP数据协议)并处理IP数据报 */
    while(1)
    {
        /* 接收包含TCP协议的IP数据报 */
        len = recvfrom(sockfd, buf, sizeof(buf),0,NULL,NULL);
 
        eth = (struct ether_header *)buf;
        ether_type = htons(eth->ether_type);
        switch(ether_type){
        case ETHERTYPE_IP:
            printf("IP协议\n");
            break;
        case ETHERTYPE_ARP:
            printf("ARP协议\n");
            break;
        case ETHERTYPE_LOOPBACK:
            printf("loop back\n");
            break;
        default:
            printf("其他协议 %x\n",eth->ether_type);
        }
        if(ether_type != ETHERTYPE_IP)
            continue;
 
        /* 打印源IP和目的IP */
        iph = (struct iphdr *)(buf+14);
        if(iph->protocol != IPPROTO_TCP)
            continue;
        printf("源IP:%s\n",inet_ntoa(*(struct in_addr *)&iph->saddr) );
        printf("目的IP%s\n",inet_ntoa(*(struct in_addr *)&iph->daddr) );
 
        /* 打印TCP包头的源端口号和目的端口号 */
        tcph = (struct tcphdr *)(buf+14+iph->ihl*4);
        printf("%hu--->", ntohs(tcph->source));
        printf("%hu\n", ntohs(tcph->dest));
 
        /* 打印TCP数据段的长度 */
        printf("TCP首部长度:%d\n", tcph->doff*4);
    }
    //关闭套接字
    close(sockfd);
    return 0;
}

域名解析函数

gethostbyname函数

struct hostent
{
  char * h_name;/*官方域名*/
  char ** h_aliases;/*别名*/
  int h_addrtype;/*地址族(地址类型)*/
  int h_length;/*地址长度*/
  char ** h_addr_list;/*地址列表*/
}

#define h_addr h_addr_list[0]/*实现向后兼容性

结构的成员

  • h_name:主机的正式名称
  • h_aliases:主机的备用名称数组,以NULL结尾指针
  • h_addrtype:地址类型;(AF_INET或AF_INET6)
  • h_length:地址的长度(以字节为单位)
  • h_addr_list:指向主机网络地址的指针数组(按网络字节顺序),由NULL指针终止
  • h_addr h_addr_list:中的第一个地址,以实现向后兼容性
#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
 
int main(int argc, char *argv[])
{
    int i;
    if(argc < 2){
        printf("%s <host name>\n", argv[0]);
        exit(0);
    }
 
    struct hostent *host = gethostbyname(argv[1]);
 
    for(i = 0; host->h_aliases[i] != NULL; i++){
        printf("%s\n", host->h_aliases[i]);
    }
    printf("Address type:%s\n",
            host->h_addrtype == AF_INET ? "AF_INET":"AF_INET6");
 
    for(i = 0; host->h_addr_list[i] != NULL; i++){
        printf("IP address %d:%s\n", i, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
    }
    endhostent();
    return 0;
} 

万维网服务器

HTTP的操作过程

在这里插入图片描述

http服务器的实现

server.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <strings.h>

#define PORT 80
#define BACKLOG 5
#define HTTPFILE "http-head.txt"
#define HTMLFILE "home.html"

int ClientHandle(int newfd);

int main(int argc, char *argv[])
{
	int fd, newfd;
	struct sockaddr_in addr;
	/*创建套接字*/
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0){
		perror("socket");
		exit(0);
	}
	int opt = 1;
	if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void *) &opt, sizeof(opt) ))
		perror("setsockopt");

	addr.sin_family = AF_INET;
	addr.sin_port = htons(PORT);
	addr.sin_addr.s_addr = 0;
	/*绑定通信结构体*/
	if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
		perror("bind");
		exit(0);
	}
	/*设置套接字为监听模式*/
	if(listen(fd, BACKLOG) == -1){
		perror("listen");
		exit(0);
	}
	/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
	newfd = accept(fd, NULL, NULL);
	if(newfd < 0){
		perror("accept");
		exit(0);
	}
	ClientHandle(newfd);
	close(fd);
	return 0;
}

int ClientHandle(int newfd){
	int file_fd = -1;
	char buf[BUFSIZ] = {};
	int ret;

	do {
		ret = recv(newfd, buf, BUFSIZ, 0);
	}while(ret < 0 && errno == EINTR);
	if(ret < 0){
		perror("recv");
		exit(0);
	}else if(ret == 0){
		close(newfd);
		return 0;
	}else{
		printf("=====================================\n");
		printf("%s", buf);
		fflush(stdout);
	}

	bzero(buf, ret);
	file_fd = open(HTTPFILE, O_RDONLY);
	if(file_fd < 0){
		perror("open");
		exit(0);
	}
	ret = read(file_fd, buf, BUFSIZ);
	printf("%s\n", buf);
	send(newfd, buf, ret, 0);
	close(file_fd);

	bzero(buf, ret);
	file_fd = open(HTMLFILE, O_RDONLY);
	if(file_fd < 0){
		perror("open");
		exit(0);
	}
	ret = read(file_fd, buf, BUFSIZ);
	printf("%s\n", buf);
	send(newfd, buf, ret, 0);
	close(file_fd);

	close(newfd);
	return 0;
}

home.html

<html>
<head>
<title>server name</title>
<meta charset="utf-8">
</head>
<body>
<body>
<h1>文档标题</h1>
<p>Hello World!</p>
</body>
</body>
</html>

http-head.txt

HTTP/1.1 200 OK 
Content-Type: text/html 
Connection: close

项目:云盘-自动云同步

需求分析

  • 文件的上传和下载
  • 文件的大小不确定
  • 文件的个数不确定
  • 实时同步需要获取文件事件
  • 定时同步需要设置定时器

如何实现手动同步

  • 实现TCP通信
  • 使用TCP实现文件的上传和下载
  • 实现整个目录下的文件的同步
  • 实现项目框架
  • 完成项目

1.实现TCP通信

server.c

#include "tcp.h"
		   
int main(int argc,char *argv[])
{
	int fd,ret,newfd;
	Addr_in newaddr,myaddr;
	socklen_t addrlen,address_len;
	/*检查参数是否为3*/
	Argment(argc,argv);
	/*创建套接字*/
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
		handle_error("socket");
	/*设置地址族协议*/
	bzero(&myaddr,sizeof(myaddr));//清空结构体
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(atoi(argv[2]));//主机字节序转网络字节序
	inet_aton(argv[1],&myaddr.sin_addr);

	/*地址快速重启*/
	int flag = 1;
	if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag)))
		handle_error("setsockopt");
	/*绑定地址族协议*/
	addrlen = sizeof(Addr_in);
	if((ret = bind(fd,(Addr *)&myaddr,addrlen)) < 0)
		handle_error("bind");
	/*设置为监听*/
	if((ret = listen(fd,BACKLOG)) < 0)
		handle_error("listen");
	
	/*等待接受客户端的连接*/
	bzero(&newaddr,sizeof(newaddr));
	address_len = sizeof(Addr_in);
	if((newfd = accept(fd,(Addr *)&newaddr,&address_len)) < 0)
		handle_error(
"accept");
	else
		printf("<%s><%d>已连接\n",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port));
	/*处理客户端的数据*/
	while(1)
	{
		char buf[BUFSIZ] = {};
		do
		{
			ret = recv(newfd,buf,BUFSIZ,0);
		}while(ret < 0 && errno == EINTR);//如果recv在接受过程中 被信号中断 则可以再次接受数据
		if(ret < 0)
			handle_error("recv");
		if(ret == 0)//如果ret=0 表示传输结束 程序退出
		{
			printf("<%s><%d>断开连接\n",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port));
			break;
		}
		if(ret > 0)
			printf("<%s><%d>:%s",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port),buf);
	}

	close(fd);
	close(newfd);
	return 0;
}

client.c

#include "tcp.h"

#define handle_error(msg) do{perror(msg);exit(EXIT_FAILURE);}while(0)
typedef struct sockaddr  Addr;
typedef struct sockaddr_in  Addr_in;
		   
int main(int argc,char *argv[])
{
	int fd,ret;
	char buf[BUFSIZ] = {"hello world!\n"};
	/*检查参数*/
	Argment(argc,argv);
	fd = SocketInit(argv);
	/*向服务端发送数据*/
	while(1)
	{	
		do 
		{
			ret = send(fd, buf, BUFSIZ, 0);
		}while(ret < 0 && errno == EINTR); //如果信号导致的错误,继续执行
		if(ret < 0)
			handle_error("recv");
		else if(!ret)
			break;
		printf("send data:%s", buf);
		fflush(stdout);
		getchar();

	}

	close(fd);
	return 0;
}

tcp.c

#include "tcp.h"
void Argment(int argc,char *argv[])
{
	/*检查参数*/
	if(argc < 3){
		fprintf(stderr, "%s <addr><port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
}

int SocketInit(char *argv[])
{
	int fd;
	Addr_in myaddr;
	/*创建套接字*/
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
		handle_error("socket");
	/*设置地址族协议*/
	bzero(&myaddr,0);//清空结构体
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(atoi(argv[2]));//主机字节序转网络字节序
	inet_aton(argv[1],&myaddr.sin_addr);
	/*向服务端请求连接*/
	if(connect(fd,(Addr *)&myaddr,sizeof(Addr_in)))
		handle_error("connect");

	return fd;

}

tcp.h

#include <stdio.h>
#include <strings.h>
#include <sys/types.h>			
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>


#define BACKLOG 5
#define handle_error(msg) do{perror(msg);exit(EXIT_FAILURE);}while(0)
typedef struct sockaddr  Addr;
typedef struct sockaddr_in  Addr_in;

void Argment(int argc,char *argv[]);
int SocketInit(char *argv[]);

2.TCP函数封装

server.c

#include "tcp.h"
		   
int main(int argc,char *argv[])
{
	int fd,ret,newfd;
	Addr_in newaddr;
	socklen_t address_len;
	char buf[BUFSIZ] = {};
	/*检查参数是否为3*/
	Argment(argc,argv);
	/*套接字初始化*/
	fd = SocketInit(argv,SERVER);
	/*等待接受客户端的连接*/
	bzero(&newaddr,sizeof(newaddr));
	address_len = sizeof(Addr_in);
	if((newfd = accept(fd,(Addr *)&newaddr,&address_len)) < 0)
		handle_error(
"accept");
	else
		printf("<%s><%d>已连接\n",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port));
	/*处理客户端的数据*/
	while(1)
	{
		ret = SocketDataHandle(newfd,buf,BUFSIZ,(DataHand_t)recv);
		if(ret == 0)//如果ret=0 表示传输结束 程序退出
		{
			printf("<%s><%d>断开连接\n",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port));
			break;
		}
		if(ret > 0)
			printf("<%s><%d>:%s",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port),buf);
		getchar();
	}
	
	close(fd);
	close(newfd);
	return 0;
}

client.c

#include "tcp.h"
		   
int main(int argc,char *argv[])
{
	int i,fd,ret;
	char buf[BUFSIZ] = {"hello world!\n"};
	/*检查参数*/
	Argment(argc,argv);
	fd = SocketInit(argv,CLIENT);
	/*向服务端发送数据*/
	i = 0;
	while(1)
	{	
		ret = SocketDataHandle(fd,buf,BUFSIZ,(DataHand_t)send);
		if(!ret)
			break;
		printf("data:%s",buf);
		fflush(stdout);
		i++;
		if(i == 2)
			break;
//		getchar();
	}

	close(fd);
	return 0;
}

tcp.c

#include "tcp.h"
void Argment(int argc,char *argv[])
{
	/*检查参数*/
	if(argc < 3){
		fprintf(stderr, "%s <addr><port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
}

int SocketInit(char *argv[],bool server)
{
	int fd;
	Addr_in myaddr;
	fun_t = server ? bind:connect;//fun_t 函数指针 当server为0fun_t为bind的函数指针 反之则为connect函数的指针
	/*创建套接字*/
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
		handle_error("socket");
	/*设置地址族协议*/
	bzero(&myaddr,0);//清空结构体
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(atoi(argv[2]));//主机字节序转网络字节序
	if(inet_aton(argv[1], &myaddr.sin_addr) == 0)
	{
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}
	/*绑定地址族或者连接服务端*/
	if(fun_t(fd,(Addr *)&myaddr,sizeof(Addr_in)))
		handle_error("fun_t");

	/*server端调用*/
	if(server)
	{
		/*地址快速重启*/
		int flag = 1;
		if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag)))
			handle_error("setsockopt");
		/*设置为监听*/
		if(listen(fd,BACKLOG))
			handle_error("listen");
	}
	return fd;

}

int SocketDataHandle(int fd, void *buf, size_t len, DataHand_t datahandle)
{
	int ret;
	char* str = datahandle == recv ? "recv" : "send";
	do
	{
		ret = datahandle(fd,buf,len,0);
	}while(ret < 0 && errno == EINTR);//如果recv在接受过程中 被信号中断 则可以再次接受数据
	if(ret < 0)
		handle_error(str);
	return ret;
}

tcp.h

#include <stdio.h>
#include <strings.h>
#include <sys/types.h>			
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>

#define CLIENT 0
#define SERVER 1
#define BACKLOG 5
#define handle_error(msg) do{perror(msg);exit(EXIT_FAILURE);}while(0)

typedef struct sockaddr  Addr;
typedef struct sockaddr_in  Addr_in;

int (* fun_t)(int ,const Addr *,socklen_t);//bind和connect的函数指针
typedef ssize_t(* DataHand_t)(int, void *, size_t, int);//recv和send的函数指针

void Argment(int argc,char *argv[]);//检查参数
int SocketInit(char *argv[],bool server);//套接字初始化
int SocketDataHandle(int fd, void *buf, size_t len, DataHand_t datahandle);//套接字处理数据

完成代码见

C:\Users\asus\Desktop\新建文件夹\资料\嵌入式\笔记\项目\自动云同步-手动同步

2.TCP函数封装

server.c

#include "tcp.h"
		   
int main(int argc,char *argv[])
{
	int fd,ret,newfd;
	Addr_in newaddr;
	socklen_t address_len;
	char buf[BUFSIZ] = {};
	/*检查参数是否为3*/
	Argment(argc,argv);
	/*套接字初始化*/
	fd = SocketInit(argv,SERVER);
	/*等待接受客户端的连接*/
	bzero(&newaddr,sizeof(newaddr));
	address_len = sizeof(Addr_in);
	if((newfd = accept(fd,(Addr *)&newaddr,&address_len)) < 0)
		handle_error(
"accept");
	else
		printf("<%s><%d>已连接\n",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port));
	/*处理客户端的数据*/
	while(1)
	{
		ret = SocketDataHandle(newfd,buf,BUFSIZ,(DataHand_t)recv);
		if(ret == 0)//如果ret=0 表示传输结束 程序退出
		{
			printf("<%s><%d>断开连接\n",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port));
			break;
		}
		if(ret > 0)
			printf("<%s><%d>:%s",inet_ntoa(newaddr.sin_addr),ntohs(newaddr.sin_port),buf);
		getchar();
	}
	
	close(fd);
	close(newfd);
	return 0;
}

client.c

#include "tcp.h"
		   
int main(int argc,char *argv[])
{
	int i,fd,ret;
	char buf[BUFSIZ] = {"hello world!\n"};
	/*检查参数*/
	Argment(argc,argv);
	fd = SocketInit(argv,CLIENT);
	/*向服务端发送数据*/
	i = 0;
	while(1)
	{	
		ret = SocketDataHandle(fd,buf,BUFSIZ,(DataHand_t)send);
		if(!ret)
			break;
		printf("data:%s",buf);
		fflush(stdout);
		i++;
		if(i == 2)
			break;
//		getchar();
	}

	close(fd);
	return 0;
}

tcp.c

#include "tcp.h"
void Argment(int argc,char *argv[])
{
	/*检查参数*/
	if(argc < 3){
		fprintf(stderr, "%s <addr><port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
}

int SocketInit(char *argv[],bool server)
{
	int fd;
	Addr_in myaddr;
	fun_t = server ? bind:connect;//fun_t 函数指针 当server为0fun_t为bind的函数指针 反之则为connect函数的指针
	/*创建套接字*/
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
		handle_error("socket");
	/*设置地址族协议*/
	bzero(&myaddr,0);//清空结构体
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(atoi(argv[2]));//主机字节序转网络字节序
	if(inet_aton(argv[1], &myaddr.sin_addr) == 0)
	{
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}
	/*绑定地址族或者连接服务端*/
	if(fun_t(fd,(Addr *)&myaddr,sizeof(Addr_in)))
		handle_error("fun_t");

	/*server端调用*/
	if(server)
	{
		/*地址快速重启*/
		int flag = 1;
		if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag)))
			handle_error("setsockopt");
		/*设置为监听*/
		if(listen(fd,BACKLOG))
			handle_error("listen");
	}
	return fd;

}

int SocketDataHandle(int fd, void *buf, size_t len, DataHand_t datahandle)
{
	int ret;
	char* str = datahandle == recv ? "recv" : "send";
	do
	{
		ret = datahandle(fd,buf,len,0);
	}while(ret < 0 && errno == EINTR);//如果recv在接受过程中 被信号中断 则可以再次接受数据
	if(ret < 0)
		handle_error(str);
	return ret;
}

tcp.h

#include <stdio.h>
#include <strings.h>
#include <sys/types.h>			
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>

#define CLIENT 0
#define SERVER 1
#define BACKLOG 5
#define handle_error(msg) do{perror(msg);exit(EXIT_FAILURE);}while(0)

typedef struct sockaddr  Addr;
typedef struct sockaddr_in  Addr_in;

int (* fun_t)(int ,const Addr *,socklen_t);//bind和connect的函数指针
typedef ssize_t(* DataHand_t)(int, void *, size_t, int);//recv和send的函数指针

void Argment(int argc,char *argv[]);//检查参数
int SocketInit(char *argv[],bool server);//套接字初始化
int SocketDataHandle(int fd, void *buf, size_t len, DataHand_t datahandle);//套接字处理数据
  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值