网络基础上

网络基础上

网络分层:

1.每一层实现不同的功能,对上层的数据做透明传输

2.每一层向上层提供服务,同时使用下层提供的服务

网络通信结构为什么要分层?

1、各层是独立的

2、灵活性好

3、结构上易分割

4、易于实现和维护

5、能促进标准化工作

OSI七层模型:

img

img

交换机:

二层交换机

三层交换机

请问交换机和路由器的实现原理分别是什么?分别在哪个层次上面实现的?(自己扩展)

交换机中传的是帧。通过存储转发来实现的。

一般意义上说交换机是工作在数据链路层。但随着科技的发展,现在有了三层交换机,三层交换机已经扩展到了网络层。也就是说:它等于“数据链路层 和 部分网络层”。

路由器主要是选址和路由,是工作在网络层。路由器中传的是IP数据报。

路由器的功能及工作原理 - 知乎

各层典型协议:

\1. 网络接口与物理层

MAC地址: 48位全球唯一,网络设备的身份标识

ARP/RARP:

ARP: IP地址----->MAC地址

RARP: MAC地址--->IP地址

PPP协议: 拨号协议(GPRS/3G/4G)

2.网络层:

IP地址

IP: Internet protocol(分为IPV4和IPV6)

ICMP: Internet控制管理协议,ping命令属于ICMP

IGMP: Internet分组管理协议,广播、组播

3.传输层:

TCP: (Transfer Control protocol,传输控制协议) 提供面向连接的,一对一的可靠数据传输的协议

即数据无误、数据无丢失、数据无失序、数据无重复到达的通信

适用情况:

  1. 适合于对传输质量要求较高,以及传输大量数据的通信。

  2. 在需要可靠数据传输的场合,通常使用TCP协议

  3. MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

UDP: (user Datagram Protocol, 用户数据报协议): 提供不可靠,无连接的尽力传输协议

是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

img

SCTP: 是可靠传输,是TCP的增强版,它能实现多主机、多链路的通信

与TCP相比,SCTP是多链路的,对于TCP来说由于A端到B端是一对一单链路的,途中要经过很多路由器,一旦有一个路由器断开,信息将无法传输,而SCTP可以实现多主机多链路传输,存活性高

4.应用层:

网页访问协议:HTTP/HTTPS

邮件发送接收协议: POP3(收)/SMTP(发) 、IMAP(可接收邮件的一部分)

FTP,

Telnet/SSH: 远程登录

嵌入式相关:

NTP: 网络时钟协议

SNMP: 简单网络管理协议(实现对网络设备集中式管理)

RTP/RTSP:用传输音视频的协议(安防监控)

网络基础下

网络预备知识:

socket:

socket是一个应用编程的接口,它是一种特殊的文件描述符(对它执行IO的操作函数,比如,read(),write(),close()等操作函数)(充当一个网络插口)

TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口。套接字用(IP地址:端口号)表示,区分不同应用程序进程间的网络通信和连接,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号

socket代表着网络编程的一种资源

socket的类型:

流式套接字(SOCK_STREAM): 唯一对应着TCP 提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。数据报套接字(SOCK_DGRAM):唯一对应着UDP 提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。原始套接字(SOCK_RAW):(对应着多个协议,发送穿透了传输层) 可以对较低层次协议如IP、ICMP直接访问

socket位于应用层与传输层之间,对于原始套接字而言,例如ping命令直接跨过传输层与ICMP访问

socket位置:

img

IP地址

1.IP地址分为IPV4和IPV6

IPV4:采用32位的整数来表示

IPV6:采用了128位整数来表示

mobileIPV6: local IP(本地注册的IP),roam IP(漫游IP)

IP地址是Internet中主机的标识

* Internet中的主机要与别的机器通信必须具有一个IP地址

* IP地址为32位(IPv4)或者128位(IPv6)

* 每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由

特殊IP地址:

局域网IP: 192.XXX.XXX.XXX 10.XXX.XXX.XXX

广播IP: xxx.xxx.xxx.255, 255.255.255.255(全网广播)

组播IP: 224.XXX.XXX.XXX~239.xxx.xxx.xxx

img

* A类:前8位为网络地址,后24位为主机地址,网络位第一位必须是0,因此该类IP地址中网络ID的长度为8位,主机ID的长度为24位,该类IP地址范围为1.0.0.0~126.255.255.255,其子网掩码为255.0.0.0。(全部是0的(0.0.0.0)是指所有网络所以排除,127.0.0.0~127.255.255是环回地址)

* B类:前16位为网络地址,后16位为主机地址。网络位的前2为必须是10,因为该类IP地址中网络ID的长度为16位,主机ID的长度为16位,该类IP地址范围为128.0.0.0~191.255.255.255,其子网掩码为255.255.0.0.

* C类:前24位为网络地址,后8位主机地址。网络位的前3位必须是110,因此该类IP地址中网络ID的长度为24位,主机长度为8位。该类IP地址范围为192.0.0.0~223.255.255.255 、其子网掩码为255.255.255.0.

* D类:该类IP地址的第一个字节以1110开始,它是一个专门保留的地址,并不指向特定的网络。目前这类IP地址被用在组播中,其地址范围为224.0.0.0~239.255.255.255

* E类:该类IP地址以11110开始,为保留地址。其地址范围为240.0.0.0~255.255.255.254

* 此外,还有全0和全1的IP地址,其中全部是0的(0.0.0.0)是指所有网络,全1的IP地址(255.255.255.255)是所有网络的广播地址

端口号

为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别

端口号一般由IANA (Internet Assigned Numbers Authority) 管理

16位的数字(1-65535)

众所周知端口: 1~1023(FTP: 21,SSH: 22, HTTP:80, HTTPS:469,1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)

保留端口: 1024-5000(不建议使用)

可以使用的:5000~65535

TCP端口和UDP端口是相互独立的

网络里面的通信是由IP地址+端口号来决定 的

什么是端口号?

在谈论端口号之前我们必须先明白了解运输层的作用:

运输层:为相互通信的应用程序提供逻辑通信

img

我们都知道,在IP层协议能够把源主机A发出的分组,按照源IP地址,送到目的IP地址,那么,传输层是做什么的呢?

从网络层来说,通信的是两个主机(两个局域网),IP数据报的首部明确标志了这两台主机的IP地址,但这是两台主机的沟通远远不够,因为真正需要通信的是两台主机上的进程。IP协议仅仅能够把数据传到目的主机,但这远远不够,这个分组仅仅停留在了主机的网络层而没有交付到主机的应用层。

从运输层来看,通信的真正端点并不是主机而是“主机的进程”

所以,传输层和网络层的明显区别是:网络层为主机之间提供逻辑通信,而运输层提供端到端的逻辑通信

什么是端口?

我们之前在初识进程中知道,单个计算机进程是用进程标示符(PID)标志的。但是在互联网的大环境下,操作系统很多,不同的操作系统有不同的进程标识符,所以仅仅用进程标示符是不足够的。

因此,为了让不同操作系统的计算机应用程序能够互相通信,就必须用统一的方法对进程进行标志

但就算使用统一的标示符进行标识,也存在问题

1.进程的创建和撤销是动态的,通信的一方几乎无法识别对方的进程

2.我们需要主机提供的功能来识别通信的重点,但是我们无法识别具体的进程是哪个

所以:运输层使用“”协议端口号“来解决这个问题,就是端口号。

端口号解决了传输层的分用问题

————————————————

版权声明:本文为CSDN博主「胡思先生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:什么是端口号?_端口号什么意思-CSDN博客

字节序:

字节序是指不同的CPU访问内存中的多字节数据时,存在大小端问题

如果CPU访问字符串时,则不存在大小端问题

img

一般来说:

X86/ARM:小端模式

powerpc/mips:ARM作为路由器,大端模式

网络传输时采用大端模式(网络字节序)

本地字节序和网络字节序:

  • 在大部分PC机上,当应用进程将整数送入socket前,需要转化成网络字节序;当应用进程从socket取出整数后,要转化成小端字节序(原因?)

把给定系统所采用的字节序称为主机字节序。为了避免不同类别主机之间在数据交换时由于对于字节序的不同而导致的差错,引入了网络字节序

img

img

IP地址转换接口:

#include <sys/socket.h>
​
#include <netinet/in.h>
​
#include <arpa/inet.h>
​
​
in_addr_tinet_addr(const char *cp);
​
cp:点分形式的IP地址(以\0结尾的点分IPV4形式的字符串),返回值是32位整数(内部已包含了字节序转换,默认为网络字节序的模式)
​
特点:
​
仅适用于IPV4
​
当出错时,返回-1

此函数不能用于255.255.255.255的转换(该点分形式为特殊情况,转换结果为-1的32位形式,认为出错)

#include <arpa/inet.h>
​
​
 int inet_pton(int af, const char *src, void *dst);
​
​
特点:
​
适用于IPV4和IPV6
​
能正确处理255.255.255.255的转换问题
​
参数:
​
af:地址协议族(AF_INET或AF_INET6)
​
src:是一个指针(填写点分形式的IP地址[主要指IPV4])
​
dst:存放转换结果(内部已包含字节序转换,默认为网络字节序的模式)
​
returns 1 on success (network address was successfully converted).  0 is returned if src does not contain a character string repre‐
​
•       senting a valid network address in the specified address family.  If af does not contain a valid address family, -1 is returned  and  errno  is
​
•       set to EAFNOSUPPORT.
​
即成功返回1,不成功返回0或-1

#include <arpa/inet.h>
​
​
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);                        
​
特点:
​
适用于IPV4和IPV6
​
能正确处理-1的转换问题
​
参数:
​
af:地址协议族(AF_INET或AF_INET6)
​
src:是一个指针(32位网络字节序形式的IP地址)
​
dst:输出结果为点分形式的IP地址[主要指IPV4]
​
返回值:
​
On success, inet_ntop() returns a non-NULL pointer to dst.  NULL is returned if there was an error, with errno set to indicate the error.
​

TCP编程

TCP编程API:

img

1.socket:

#include <sys/types.h>          
​
#include <sys/socket.h>
​
 int socket(int domain, int type, int protocol);  //创建套接字
​
​
参数:
​
domain:
​
•       AF_UNIX, AF_LOCAL   Local communication              unix(7)
​
•       AF_INET             IPv4 Internet protocols          ip(7)      //IPV4
​
•       AF_INET6            IPv6 Internet protocols          ipv6(7)  //IPV6
​
•       AF_IPX              IPX - Novell protocols
​
•       AF_NETLINK          Kernel user interface device     netlink(7)  //内核与用户空间的通信
​
•       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
​
•       AF_AX25             Amateur radio AX.25 protocol
​
•       AF_ATMPVC           Access to raw ATM PVCs
​
•       AF_APPLETALK        Appletalk                        ddp(7)
​
•       AF_PACKET           Low level packet interface       packet(7)
​
​
type:
​
SOCK_STREAM    流式套接字  唯一对应于TCP
​
SOCK_DGRAM    数据报套接字 唯一对应于UDP
​
SOCK_RAW         原始套接字
​
​
protocol   :一般填0,原始套接字编程时填充
​
​
返回值:
​
On  success,  a  file  descriptor  for  the new socket is returned.  On
​
error, -1 is returned, and errno is set appropriately.
​

2.bind函数

原型:
​
#include <sys/types.h>       
​
#include <sys/socket.h>
​
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);  //绑定本机地址和端口
​
参数:
​
   sockfd: 通过socket()函数拿到的fd
​
   addr: struct sockaddr的结构体变量的地址
​
   addrlen: 地址长度
​
​
返回值:成功返回0,出错返回-1

struct sockaddr {
​
•               sa_family_t sa_family;  2字节
​
•               char        sa_data[14];  14字节
​
•           }
​
man socket可查看

struct sockaddr_in {
​
•               sa_family_t    sin_family; /* address family: AF_INET */
​
•               in_port_t      sin_port;   /* port in network byte order */
​
•               struct in_addr sin_addr;   /* internet address */
•           };
​
​
​
•           /* Internet address. */
​
•           struct in_addr {
​
•               uint32_t       s_addr;     /* address in network byte order */
​
•           };
​
man 7 ip可查看

也可以看作下面的:

struct sockaddr_in
​
  {          
​
•       u_short sin_family;      // 地址族, AF_INET,2 bytes
​
•       u_short sin_port;      // 端口,2 bytes
​
•       struct in_addr sin_addr;  // IPV4地址,4 bytes  
​
•       char sin_zero[8];        // 8 bytes unused,作为填充
​
  };
​
struct in_addr
​
{
•     in_addr_t  s_addr;            // u32 network address
​
};

struct sockaddr是通用的套接字地址形式,而不同的网络体系表示地址的方法是不同的;

struct sockaddr_in是在通用的套接字地址形式的基础上,针对TCP/IP设计的套接字的地址形式,里面多了两个成员,也就是IP号和端口号,这样方便我们填充。

具体的类比图如下,可以看出两个结构体的大小是一样的。

img

img

sin_family is always set to AF_INET

img

bzero(&sin,sizeof(sin));是将从&sin首地址开始的sizeof(sin)个字节全部用0填充

与memset(&sin,0,sizeof(sin))类似

附:==bzero与memset函数对比==:

1、bzero()好记忆:2个参数;

2、memset()易出错:3个参数,且第二、三个参数易记混淆,如若出现位置互换的情况,C编译器并不能察觉。

使用

1、bzero()

函数原型:extern void bzero(void *s, int n)

头文件:<string.h>

功能:置字节字符串s的前n个字节为零且包括‘\0’

说明:无返回值

2、memset()

函数原型:extern void *memset(void *buffer, int c, int count)

头文件:<string.h>

功能:把buffer所指内存区域的前count个字节设置成c的值。

例程:

#include <string.h>

void main(int argc, char *argv[])

{

struct sockaddr_in servaddr;//套接字地址结构

bzero( &servaddr, sizeof(servaddr) );

// memset( &servaddr, 0, sizeof(servaddr) );

/*

bzero( &servaddr, 5 );

memset( &servaddr, 0, 5 );//假如 0 和 5 位置记错了,C编译器并不会报错(参数类型相同)

*/

}

区别

1、bzero()不是ANSI C函数,其起源于早期的Berkeley网络编程代码,但是几乎所有支持套接字API的厂商都提供该函数;

2、memset()为ANSI C函数,更常规、用途更广。

————————————————

版权声明:本文为CSDN博主「liuxu324」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:memset()和bzero()的使用和区别_bzero和memset-CSDN博客

另外,如果是IPV6的编程,要使用struct sockddr_in6结构体(详细情况请参考man 7 ipv6),通常更通用的方法可以通过struct sockaddr_storage来编程

3.listen

#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>

int listen(int sockfd, int backlog);    //主动套接字变为被动套接字

参数:
​        sockfd: 通过socket()函数拿到的fd

​        backlog: 同时允许几路客户端和服务器进行正在连接的过程(正在三次握手)

​       一般填5, 测试得知,ARM最大为8

内核中服务器的套接字fd会维护2个链表:

1 正在三次握手的的客户端链表(数量=2*backlog+1)

2.已经建立好连接的客户端链表(已经完成3次握手分配好了newfd)

返回值:

On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

0成功,-1失败

完成listen()调用后,socket变成了监听socket(listening socket)(原sockfd属性被改变)

img

4.accept

#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>


int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);   //阻塞等待客户端连接请求


参数:

   sockfd: 经过前面socket()创建并通过bind(),listen()设置过的fd

​         addr和addrlen: 获取连接过来的客户的信息


返回值:

​       RETURN VALUE

​       On  success,  these system calls return a nonnegative integer that is a descriptor for the accepted socket.  On

​       error, -1 is returned, and errno is set appropriately.

成功时返回已经建立好连接的新的newfd

5.客户端连接函数connect()

#include <sys/types.h>    

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

客户端与服务器交互代码实现:

==tcp-net.h==

#ifndef __TCP_NET__H__

#define __TCP_NET__H__


#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <strings.h>

#include <sys/types.h>          

#include <sys/socket.h>

#include <netinet/in.h>

#include <netinet/ip.h>

#include <errno.h>

#include <arpa/inet.h>


#define SERV_PORT   5001

#define SERV_ADDR   "192.168.68.134"

#define BACKLOG     5

#define QUIT        "quit"

#endif

==tcp-client.c==

#include "tcp-net.h"


int main()

{
​    int fd;

​    struct sockaddr_in sin;


​    /* socket fd*/


​    if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }

​    /* 连接服务器*/


​    /* 填充struct socket_addr_in 结构体变量*/


​    bzero(&sin,sizeof(sin));

​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);

#if 0    

​    sin.sin_addr.s_addr = inet_addr(SERV_ADDR);

#else

​    if((inet_pton(AF_INET,SERV_ADDR,(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }

#endif


​    if((connect(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("connect");

​        exit(1);

​    }

​    /*写入*/
​    char buf[BUFSIZ];

​    while(1)

​    {
​        bzero(buf,BUFSIZ);

​        if((fgets(buf,BUFSIZ-1,stdin)) == NULL)//不一定真的出错了

​        {

​            continue;

​        }

​      
​        write(fd,buf,strlen(buf));





​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))//用户输入了quit字符

​        {

​            printf("client is exiting\n");

​            break;

​        }

​    }

​    /* 关闭文件描述符*/



​    close(fd);



​    return 0;

}

==tcp-server.c==

#include "tcp-net.h"



int main()

{

​    int fd;

​    struct sockaddr_in sin;


​    /* socket fd*/

​    if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    /* 绑定*/

​    /* 填充struct socket_addr_in 结构体变量*/


​    bzero(&sin,sizeof(sin));

​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);

#if 1    

​    /* 使服务器程序能够在任意的IP地址上运行*/

​    sin.sin_addr.s_addr = htonl(INADDR_ANY);

#else

​    if((inet_pton(AF_INET,SERV_ADDR,(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }

#endif

​    /* 绑定*/


​    if((bind(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("bind");

​        exit(1);

​    }


​    /*调用listen把主动套接字转为被动套接字*/


​    if((listen(fd,BACKLOG)) < 0)

​    {

​        perror("listen");

​        exit(1);

​    }


​    /*阻塞等待客户端连接请求*/


​    int newfd;

​    struct sockaddr_in cin;

​    socklen_t addrlen;


#if 0

​    if((newfd = accept(fd,NULL,NULL)) <0)

​    {

​        perror("accept");

​        exit(1);

​    }

#else

​    /* 优化:通过程序获取刚建立连接的socket的客户端的IP地址和端口号*/


​    if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0)

​    {

​        perror("accept");

​        exit(1);

​    }



​    char ipv4_addr[16];

​    

​    if(!inet_ntop(AF_INET,(void *)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin)))

​    {

​        perror(inet_ntop);

​        exit(1);

​    }


​    printf("client(%s:%d) is connected\n",ipv4_addr,ntohs(cin.sin_port));

#endif

​    /*读写*/

​    char buf[BUFSIZ];

​    int ret;


​    while(1)

​    {
​        bzero(buf,BUFSIZ);

​        do

​        {

​            ret = read(newfd,buf,BUFSIZ-1);

​        }while(ret < 0 && EINTR == errno);


​        if(ret < 0)

​        {

​            perror("read");

​            exit(1);

​        }

​      
​        if(!ret)//对方已经关闭

​        {

​            break;

​        }


​        printf("read data:%s\n",buf);


​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("client is exiting\n");

​            break;

​        }

​    }


​    close(newfd);

​    close(fd);

​    return 0;

}

代码中需要注意的地方:

BUFSIZ大小:

发现代码直接使用 BUFSIZ,经过查找,发现: stdio.h -> libio.h -> _G_config.h, 分别是 : BUFSIZ -> _IO_BUFSIZ -> _G_BUFSIZ,是 8192.

img

关于程序中的do....while循环:

errno 是read 返回之后,会把结果是否正确和错误类型记录在errno 上, 当errno等于EINTR时,是因为read处于读阻塞状态时,进程如果捕获到了一个信号,read就有可能终止阻塞返回,errno的值就是EINTR,此时不认为是错误,继续读取

INADDR_ANY:

INADDR_ANY的确切含义//**链接**

INADDR_ANY就是inet_addr("0.0.0.0")

首先,需要明确的是当服务器的监听地址是INADDR_ANY时设置的是服务器的IP地址。

其次,当服务器的监听地址是INADDR_ANY时含义是让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求。

将sin_addr设置为INADDR_ANY"的含义是什么?

问:

很多书上都说“将sin_addr设置为INADDR_ANY,则表示所有的IP地址,也即所有的计算机”,这样的解说让人费解。

答:

INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。

当服务器的监听地址是INADDR_ANY时,意思不是监听所有的客户端IP。而是服务器端的IP地址可以随意配置,这样使得该服务器端程序可以运行在任意计算机上,可使任意计算机作为服务器,便于程序移植。将INADDR_ANY换成127.0.0.1也可以达到同样的目的。这样,当作为服务器的计算机的IP有变动或者网卡数量有增减,服务器端程序都能够正常监听来自客户端的请求。我是这么理解的。

比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。

但是一定要保证客户端发送的端口号与ip地址是对的

拓展:tcp中有序传输的原理

那么,TCP具体是通过怎样的方式来保证数据的顺序化传输呢?

主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。

具体步骤如下:

(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;

(2)并为每个已发送的数据包启动一个超时定时器;

(3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;

(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。

(5)接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

TCP如何保证发送的报文是可靠有序的_tcp 后继报文有序-CSDN博客


并发服务器:

img

多进程与多线程的区别:

多进程与后者相比虽然资源消耗大些,但是稳定性会比多线程好

bind: Address already in use

当客户端保持着与服务器端的连接,这时服务器端断开,再开启服务器时会出现: Address already in use,可以用netstat -anp | more 可以看到客户端还保持着与服务器的连接(还在使用服务器bind的端口)。这是由于client没有执行close,连接还会等待client的FIN包一段时间。解决方法是使用setsockopt,使得socket可以被重用,是最常用的服务器编程要点。具体的做法为是,在socket调用和bind调用之间加上一段对socket的设置:

————————————————

版权声明:本文为CSDN博主「Michael.Scofield」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:Linux下Socket编程:bind().Address already in use的解决方法_linux中socket 127.0.0.1:7001: bind: address already-CSDN博客

需要改进:允许服务器地址快速重用

多线程并发服务器:

多线程并发服务器.zip

思路:

服务端先socket一个套接字,然后填充套接字地址结构体,绑定本机地址和端口(注意字节序的转换),调用listen使主动套接字变为被动套接字(监听套接字),用于监听从客户端来的连接请求,如果监听到连接请求,accept就从阻塞状态返回,生成一个newfd对等套接字与客户端通信,并且将客户端属性放进accept参数的客户端结构体,并创建创建一个线程处理已经建立连接的客户端数据,执行完一遍while循环后继续监听其他客户端的连接请求(阻塞),以此类推.....

==th_test_server.c:==

#include "th_test_net.h"

void *cli_data_handler(void *arg);

int main()

{

​    int fd,newfd;

​    struct sockaddr_in sin;

​    struct sockaddr_in cin;

​    socklen_t addrlen = sizeof(cin);

​    char ipv4_addr[16];

​    pthread_t tid;





​    if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }



​    /* 使服务端可以快速重用*/

​    int b_reuse = 1;





​    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));



​    /*填充*/

​    bzero(&sin,sizeof(sin));//先清空再赋值





​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);            //本地字节序转网络字节序s

​    sin.sin_addr.s_addr = htonl(INADDR_ANY);   //本地字节序转网络字节序l



​    /*绑定本机地址和端口*/

​    if((bind(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("bind");

​        exit(1);

​    }



​    /*设置为监听套接字*/

​    if((listen(fd,BACKLOG)) < 0)

​    {

​        perror("listen");

​        exit(1);

​    }



​    while(1)

​    {

​        //阻塞等待客户端链接,成功返回对等套接字与客户端属性

​        if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0)  

​        {

​            perror("accept");

​            exit(1);

​        }





​        if(!inet_ntop(AF_INET,(void *)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin)))//注意客户端ip地址转换为点分形式

​        {

​            perror("inet_ntop");

​            exit(1);

​        }





​        printf("client(%s:%d) is connected!\n\n",ipv4_addr,ntohs(cin.sin_port));//注意客户端端口号转换为本地字节序



​        /*创建线程处理已经建立好连接的客户端数据*/

​        pthread_create(&tid,NULL,cli_data_handler,(void *)&newfd);  //注意这里一定要加上&否则会报段错误

​    }





​    close(fd);//出错退出要关掉fd





​    return 0;

}





void *cli_data_handler(void *arg)

{

​    int newfd = *(int *)arg;

​    int ret;

​    char buf[BUFSIZ];





​    printf("client thread(fd=%d)\n",newfd);





​    pthread_detach(pthread_self()); //设置属性分离,用于线程回收





​    while(1)

​    {

​        bzero(buf,BUFSIZ);





​        do

​        {

​            ret = read(newfd,buf,BUFSIZ-1); //注意要用do..while循环读取,读取的字节长度保留\0

​        }while(ret < 0 && EINTR == errno);

​        





​        if(ret < 0)

​        {

​            perror("read");

​            break;

​        }





​        else if(!ret)//客户端关闭

​        {

​            break;

​        }



​        printf("read from client:%s\n",buf);



​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))//读取到用户输入quit退出

​        {

​            printf("client(%d) is exiting\n",newfd);

​            break;

​        }

​    }



​    close(newfd);//推出后关掉newfd



​    pthread_exit(NULL);

}

==th_test_client.c:==

\

#include "th_test_net.h"



/*设置用法函数,提示用户输入正确的参数*/

void usage(char *s)

{

​    printf("\n%s serv_ip serv_port\n",s);

​    printf("serv_ip : server ip address\n");

​    printf("serv_port : server port(>5000)\n\n");

}





int main(int argc,char **argv)  //采用main函数输入参数赋值法(可执行文件为第一个参数)

{

​    

​    int fd;

​    unsigned short port;

​    struct sockaddr_in sin;

​    char buf[BUFSIZ];





​    if(argc != 3)     //参数不为3,调用用法函数并退出

​    {

​        usage(argv[0]);

​        exit(1);

​    }





​    if((port = (unsigned short)atoi(argv[2])) < 5000)   //输入的端口号转换为整型在强制转换为无符号短整型,不合法调用用法函数并退出

​    {

​        usage(argv[0]);

​        exit(1);

​    }



​    /*创建客户端套接字*/

​    if((fd = socket(AF_INET,SOCK_STREAM,0)) <0)

​    {

​        perror("socket");

​        exit(1);

​    }

​    

​    /*填充*/



​    bzero(&sin,sizeof(sin)); //先清空再填充





​    sin.sin_family = AF_INET;

​    sin.sin_port =htons(port);  //注意这里要转换为网络字节序

​    

​    if((inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr)) != 1)  //点分ip地址转换为网络地址

​    {

​        perror("inet_pton");

​        exit(1);

​    }







​    if((connect(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)  //发送链接(注意发送时填充一定要正确,否则会显示被拒绝)

​    {

​        perror("connect");

​        exit(1);

​    }



​    while(1)

​    {

​        bzero(buf,BUFSIZ);





​        printf(">");

​        if(!fgets(buf,BUFSIZ-1,stdin))

​        {

​            perror("fgets");

​            continue;

​        }





​        if((write(fd,buf,strlen(buf))) < 0)

​        {

​            perror("write");

​            continue;

​        }



​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))  //无视大小写比较,相等返回0

​        {

​            printf("client(fd=%d) is exiting\n",fd);

​            break;

​        }

​    }



​    close(fd);//先关掉fd,再推出

​    

​    return 0;

}

代码中出现过的问题点:

1.如果客户端代码中newfd前的&去掉会报段错误,如下

img

原因分析:

img

客户端代码回调函数中,如果不加&取地址符号,会传入一个数值newfd并转换为void *的指针,传进去编译器不会报错然后又将该指针强制转换为int *(整形指针),最后对它取值,也就是说它是将newfd看作指针再取值,但是由于newfd未指向任何内存会出现非法访问内存

2.如果客户端代码中的端口号没有转换为网络字节序,会出现:

客户端:

img

服务器:

img

说明connect的时候端口号或者地址不匹配,填充的端口号和地址一定要对,该转换的一定要转换

多进程并发服务器:

多进程并发服务器.zip

思路:

服务端先socket一个套接字,然后填充套接字地址结构体,绑定本机地址和端口(注意字节序的转换),调用listen使主动套接字变为被动套接字(监听套接字),用于监听从客户端来的连接请求,如果监听到连接请求,accept就从阻塞状态返回,生成一个newfd对等套接字与客户端通信,并且将客户端属性放进accept参数的客户端结构体,接着fork一个子进程,该子进程调用一个函数用来处理已经建立连接的客户端数据,父进程关掉newfd后返回再次阻塞等待........

注意:1.回收子进程是必要的,并且要使用信号捕捉的方式(这样父进程不会阻塞),且最好用sigaction函数

2.子进程关闭fd,父进程关闭newfd,因为创建子进程时,父进程会复制一份到子进程中,这样父子进程都会有,关闭无用的会更节省资源

服务器:

#include "tcp-net.h"



void *cli_data_handler(void *arg);

void child_handler(int sig);



int main()

{

​    int fd;

​    struct sockaddr_in sin;


​    /*回收子进程*/

​    
​    struct sigaction act;

​    act.sa_handler = child_handler;

​    act.sa_flags = 0;

​    sigemptyset(&act.sa_mask);


​    if((sigaction(SIGCHLD,&act,NULL)) < 0)

​    {

​        perror("sigaction");

​        exit(1);

​    }


​    /* socket fd*/

​    if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    /*允许绑定地址快速重用*/

​    int b_reuse = 1;


​    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));


​    /* 绑定*/

​    /* 填充struct socket_addr_in 结构体变量*/


​    bzero(&sin,sizeof(sin));

​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);


#if 1    

​    /*优化1 使服务器程序能够在任意的IP地址上运行*/


​    sin.sin_addr.s_addr = htonl(INADDR_ANY);

#else

​    if((inet_pton(AF_INET,SERV_ADDR,(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }

#endif

​    /* 绑定*/

​    if((bind(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("bind");

​    }


​    /*调用listen把主动套接字转为被动套接字*/


​    if((listen(fd,BACKLOG)) < 0)

​    {

​        perror("listen");

​        exit(1);

​    }


​    /*阻塞等待客户端连接请求*/


​    /*优化3: 用多进程或多线程处理已经建立好连接的客户端数据*/


​    int newfd;

​    pid_t pid;

​    struct sockaddr_in cin;

​    socklen_t addrlen = sizeof(cin);

​    char ipv4_addr[16];


​    while(1)

​    {

​        if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0)

​        {

​            perror("accept");

​            exit(1);

​        }

​        /*创建一个子进程用于处理已经建立连接的客户端的交互数据*/


​        if((pid = fork()) < 0)

​        {

​            perror("fork");

​            break;

​        }

​        else if(0 == pid)

​        {
​            close(fd);


​            if(!inet_ntop(AF_INET,(void *)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin)))//为什么是cin大小的

​            {

​                perror("inet_ntop");

​                exit(1);

​            }


​            printf("client(%s:%d) is connected\n",ipv4_addr,ntohs(cin.sin_port));

​            cli_data_handler(&newfd);

​            return 0;

​        }

​        else

​        {
​            close(newfd);

​        }
​    }

​    close(fd);

​    return 0;

}


void *cli_data_handler(void *arg)

{

​    int newfd = *(int *)arg;

​    printf("child-process: newfd=%d\n",newfd);

​    /*与newfd进行数据读写*/

​    char buf[BUFSIZ];

​    int ret;


​    while(1)

​    {

​        bzero(buf,BUFSIZ);

​        do

​        {

​            ret = read(newfd,buf,BUFSIZ-1);

​        }while(ret < 0 && EINTR == errno);


​        if(ret < 0)

​        {

​            perror("read");

​            exit(1);

​        }


​        if(!ret)//对方已经关闭

​        {

​            break;

​        }



​        printf("read data:%s\n",buf);


​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("client(%d) is exiting\n",newfd);

​            break;

​        }

​    }



​    close(newfd);

}


void child_handler(int sig)

{

​    if(SIGCHLD == sig)

​    {

​        waitpid(-1,NULL,WNOHANG);

​    }

}

客户端:

#include "tcp-net.h"


void usage(char *s)

{

​    printf("\n%s serv_ip serv_port",s);

​    printf("\n\t\t serv_ip : server ip address");

​    printf("\n\t\t serv_port : server port(>5000)\n\n");

}


int main(int argc,char **argv)

{

​    int fd;

​    struct sockaddr_in sin;

​    int port;

​    

​    if(argc != 3)

​    {

​        usage(argv[0]);

​        exit(1);

​    }

​    if((port = atoi(argv[2])) < 5000)

​    {

​        usage(argv[0]);

​        exit(1);

​    }


​    /* socket fd*/

​    if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }

   
​    /* 连接服务器*/

​    
​    /* 填充struct socket_addr_in 结构体变量*/



​    bzero(&sin,sizeof(sin));

​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(port);//注意这里一定要转换

#if 0    

​    sin.sin_addr.s_addr = inet_addr(SERV_ADDR);

#else

​    if((inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }

#endif



​    if((connect(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("connect");

​        exit(1);

​    }



​    /*写入*/



​    char buf[BUFSIZ];



​    while(1)

​    {

​        bzero(buf,BUFSIZ);





​        if((fgets(buf,BUFSIZ-1,stdin)) == NULL)//不一定真的出错了

​        {

​            continue;

​        }

​        

​        if(write(fd,buf,strlen(buf)) <0)

​        {

​            perror("write");

​            exit(1);

​        }

​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))//用户输入了quit字符

​        {

​            printf("client is exiting\n");

​            break;

​        }

​        

​    }

​    /* 关闭文件描述符*/


​    close(fd);

​    return 0;

}

注意的地方基本与多线程一致,下面是遇到过的问题点:

1.多个进程中只退出一个,服务器也退出

accept:Interrupted system call

img

原因分析:

子进程退出后会有一个SIGCHLD信号,此时父进程正在阻塞等待,遇到此信号直接返回错误值退出,因此这里要加上判断

if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0)

{

if(newfd < 0 && EINTR == errno)

{

continue;

}

else

{

perror("accept");

exit(1);

}

}

img

2.warning: control reaches end of non-void function [-Wreturn-type]

用gcc编译一个程序的时候出现这样的警告:

warning: control reaches end of non-void function

它的意思是:控制到达非void函数的结尾。就是说你的一些本应带有返回值的函数到达结尾后可能并没有返回任何值。这时候,最好检查一下是否每个控制流都会有返回值。

子进程调用函数原型void *改为void即可


TCP网络编程API扩展和UDP编程:

网络发送数据:send()/write()

#include <sys/types.h>

#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

send()比write多一个参数flags:

flags:

一般填写0,此时和write()作用一样

特殊的标志:

MSG_DONTWAIT:Enables nonblocking operation; 非阻塞版本

MSG_OOB:用于发送TCP类型的带外数据(out-of-band)

网络中接受收据: recv()/read()

#include <sys/types.h>

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

flags:

一般填写0,此时和read()作用一样

特殊的标志:

MSG_DONTWAIT: Enables nonblocking operation; 非阻塞版本

MSG_OOB:用于发送TCP类型的带外数据(out-of-band)

MSG_PEEK:

This flag causes the receive operation to return data from

the beginning of the receive queue without removing that

data from the queue. Thus, a subsequent receive call will

return the same data.

img

不加MSG_PEEK的话,内核中缓冲区的数据以字符流的形式读取,前面的100个字节的数据读取完成后,后面的数据往前移动,形成字符流

加上MSG_PEEK,读取完前面100个字节的数据后会返回给你,再读第二遍又可以从刚开始的地方读取数据

UDP编程API:

img

img

img

recvfrom原型:

#include <sys/types.h>

#include <sys/socket.h>


ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); //udp



ssize_t recv(int sockfd, void *buf, size_t len, int flags);tcp扩展API

recvfrom工作流程:

需要一个while循环,刚开始阻塞等待客户端发送数据,如果监听到有数据包过来,就从阻塞中返回,并且从套接字读取数据放到缓冲区中,而客户端信息则会由系统自动装填进套接字地址结构体里面,执行下面的语句后再次返回循环,又开始阻塞等待,所以这样的话功能上就实现了一个服务器处理多个客户端的数据,类似于多进程多线程的方式。

img

recvfrom返回值:

These calls return the number of bytes received, or -1 if an error

occurred. In the event of an error, errno is set to indicate the

error. The return value will be 0 when the peer has performed an

orderly shutdown.

img

一个服务器多个客户端交互代码:

UDP编程.zip

==test-udp-server.c==

#include "test-udp-net.h"

int main()

{

​    int fd;

​    struct sockaddr_in sin;

​    struct sockaddr_in cin;

​    socklen_t addrlen = sizeof(cin);

​    char buf[BUFSIZ];

​    char ipv4_addr[16];


​    if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    bzero(&sin,sizeof(sin));


​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);

​    sin.sin_addr.s_addr = htonl(INADDR_ANY);


​    if((bind(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("bind");

​        exit(1);

​    }


​    printf("udp-server is starting......ok!\n");


​    while(1)

​    {

​        bzero(buf,BUFSIZ);

​        if((recvfrom(fd,(void *)buf,BUFSIZ-1,0,(struct sockaddr *)&cin,&addrlen)) < 0)

​        {

​            perror("recvfrom");

​            continue;

​        }

​        if(!(inet_ntop(AF_INET,(void *)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin))))

​        {

​            perror("inet_ntop");

​            break;

​        }


​        printf("receive from client(%s:%d),data:%s\n",ipv4_addr,(int)ntohs(cin.sin_port),buf);


​        if(!(strncasecmp(buf,QUIT,strlen(QUIT))))

​        {

​            printf("client is exiting\n");

​        }

​    }

​    close(fd);

​    return 0;

}

==test-udp-client.c==

#include "test-udp-net.h"



void usage(char *s)

{

​    printf("\n %s , server_ip,server_port:\n",s);

​    printf("\n ./test-udp-client : %s\n",s);

​    printf("server_ip : server ip adress\n");

​    printf("server_port : server port(>5000)\n\n");

}

int main(int argc,char **argv)

{
​    int fd;

​    unsigned short port;

​    char buf[BUFSIZ];

​    struct sockaddr_in sin;


​    if(argc != 3)

​    {

​        usage(argv[0]);

​        exit(1);

​    }


​    if((port = (unsigned short)atoi(argv[2])) < 5000)

​    {

​        usage(argv[0]);

​        exit(1);

​    }



​    if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    bzero(&sin,sizeof(sin));


​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(port);

​    if((inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr)) !=1 )

​    {

​        perror("inet_pton");

​        exit(1);

​    }



​    printf("udp-client is starting.....ok!\n");


​    while(1)

​    {

​        bzero(buf,BUFSIZ);

​        fprintf(stderr,"please input a string:\n");


​        if(!(fgets(buf,BUFSIZ-1,stdin)))

​        {

​            perror("fgets");

​            continue;

​        }


​        if((sendto(fd,(void *)buf,strlen(buf),0,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​        {

​            perror("sendto");

​            break;

​        }


​        if(!(strncasecmp(buf,QUIT,strlen(QUIT))))

​        {

​            printf("client(fd=%d) is exiting!\n",fd);

​            break;

​        }

​    }

​    close(fd);

​    return 0;

}


IO多路复用:

IO模型与IO多路复用:

img

阻塞I/O:

* 阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。

* 缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。

* 前面学习的很多读写函数在调用过程中会发生阻塞。

* 读操作中的read、recv、recvfrom

* 写操作中的write、sendto

* 其他操作:accept、connect

注意:sendto不是阻塞IO,connect是阻塞IO

read函数:(从内核缓冲区读数据)

img

img

write(往内核缓冲区里写数据)

img

img

sendto在UDP中不阻塞:因为UDP不用等待确认,他没有实际的缓冲区,不存在缓冲区被写满的情况,UDP套接字上执行的写操作永远不会阻塞

非阻塞IO:

* 当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

* 当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。

* 应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。

这种模式使用中不普遍

示意图:

img

非阻塞IO的实现:

img

IO多路复用:

上次的TCP/UDP多线程、多进程并发服务器,无论是多进程还是多线程,都会导致系统给他开辟内存资源,而且客户端有时还会长时间的空等待,造成资源浪费和大量消耗CPU,这时候可以采用多路复用的通讯模型。 多路复用模型是一个进程或线程,监听多个文件描述符,什么时候有数据什么时候去进行读写操作,不需要空等待,即选择性执行任务。

* 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;

* 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

* 若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;

* 比较好的方法是使用I/O多路复用。其基本思想是:

* 先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。

* 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

select

img

img

select是阻塞函数,select不仅可以对套接字fd进行监控,还可以监控普通文件的文件描述符fd

select函数原型:

 #include <sys/select.h>



​       /* According to earlier standards */

​       \#include <sys/time.h>

​       \#include <sys/types.h>

​       \#include <unistd.h>



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

​                



​        struct timeval {

​                       long    tv_sec;         /* seconds */

​                       long    tv_usec;        /* microseconds */

​                   };

参数:

img

* 为了设置文件描述符我们要使用几个宏:

* FD_SET 将fd加入到fdset

* FD_CLR 将fd从fdset里面清除

* FD_ZERO 从fdset中清除所有的文件描述符

* FD_ISSET 判断fd是否在fdset集合中

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);

返回值:

On success, select() and pselect() return the number of file descrip‐

tors contained in the three returned descriptor sets (that is, the

total number of bits that are set in readfds, writefds, exceptfds)

which may be zero if the timeout expires before anything interesting

happens. On error, -1 is returned, and errno is set appropriately; the

sets and timeout become undefined, so do not rely on their contents

after an error.

img

img

* 在我们调用select时进程会一直阻塞直到以下的一种情况发生.

* 有文件可以读.

* 有文件可以写.

* 超时所设置的时间到.

关键点:

\1. select( )函数里面的各个文件描述符fd_set集合的参数在select( )前后发生了变化:

前:表示关心的文件描述符集合

后:有数据的集合(如不是在超时还回情况下)

其实后者是前者的子集

\2. 那么究竟是谁动了fd_set集合的奶酪?

答曰:kernel

总结:select其实就是不断扫描关注的文件描述符表,将有数据的文件描述符统计,生成一个新的表,该表将有数据的比特位置为1,其他位清零

select弊端:

①:文件描述符有限,不适用于大规模的客户端场合

②:轮询机制繁琐,耗费资源

③:另外,客户端和服务器通信是内存拷贝的过程,服务器先从应用空间拷贝一份到内核,再传给客户端

error: stray ‘\200’ in program

linux下可以通过命令行:

img

查看结果出错的字符,出错的地方会显示乱状态

原因是代码中有中文字符,比如不小心敲了中文空格\标点符号,一般中文注释是不会的

问题:

为什么maxfd+1为4字节的整数倍?

服务器端采用select实现多并发:

IO多路复用.zip

客户端采用select:

#include "IO-select-net.h"





void usage(char *s)

{

​    printf("\n%s serv_ip serv_port\n",s);

​    printf("serv_ip : server ip address\n");

​    printf("serv_port : server port(>5000)\n\n");

}





int main(int argc,char **argv)

{



​    int fd;

​    unsigned short port;

​    struct sockaddr_in sin;

​    char buf[BUFSIZ];





​    if(argc != 3)

​    {

​        usage(argv[0]);

​        exit(1);

​    }





​    if((port = (unsigned short)atoi(argv[2])) < 5000)

​    {

​        usage(argv[0]);

​        exit(1);

​    }





​    if((fd = socket(AF_INET,SOCK_STREAM,0)) <0)

​    {

​        perror("socket");

​        exit(1);

​    }





​    bzero(&sin,sizeof(sin));





​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(port);





​    if((inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }





​    if((connect(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("connect");

​        exit(1);

​    }





​    

​    printf("IO-client is starting...ok\n");







​    fd_set rset;

​    int maxfd;

​    struct timeval timeout;

​    int ret;





​    while(1)

​    {

​        FD_ZERO(&rset);

​        FD_SET(0,&rset);

​        FD_SET(fd,&rset);

​        maxfd = fd;





​        timeout.tv_sec = 5;

​        timeout.tv_usec = 0;





​        select(maxfd+1,&rset,NULL,NULL,&timeout);





​        if(FD_ISSET(0,&rset)) //标准键盘有输入,读取标准键盘的数据写到套接字里

​        {

​            bzero(buf,BUFSIZ);





​            do

​            {

​                ret = read(0,buf,BUFSIZ-1);

​            }while(ret < 0 && EINTR == errno);





​            if(ret < 0)

​            {

​                perror("read from stdin");

​                continue;

​            }





​            else if(!ret)

​            {

​                continue;

​            }





​            else

​            {

​                if((write(fd,buf,strlen(buf))) < 0)

​                {

​                    perror("write to socket");

​                    continue;

​                }

​            }





​            if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​            {

​                printf("client(fd=%d) is exiting\n",fd);





​                break;

​            }



​        }



​        if(FD_ISSET(fd,&rset))  //服务器那边有数据过来,读取数据

​        {

​            bzero(buf,BUFSIZ);





​            do

​            {

​                ret = read(fd,buf,BUFSIZ-1);

​            }while(ret < 0 && EINTR == errno);





​            if(ret < 0)

​            {

​                perror("read from socket");

​                continue;

​            }





​            else if(!ret) //服务器关闭

​            {

​                break;

​            }





​            else

​            {

​                printf("server said:%s\n",buf);





​                /*this is a bug */

​                if((strlen(buf) > strlen(SERV_RESP)) && (!strncasecmp(buf+strlen(SERV_RESP),QUIT,strlen(QUIT))))

​                {

​                    printf(" server client is exiting\n");

​                    break;

​                }





​            }

​        }

​    }





​    close(fd);





​    return 0;

}

信号驱动IO:

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情

img

具体可参考

https://www.cnblogs.com/schips/p/12575493.html

扩展:

多路复用之Epoll:(弱项)

①:它所支持的文件描述符上限是系统默认可打开的最大文件数目

②:(通知机制)每一个fd上面都有一个callback,只有活跃的fd才会去调用callback函数

③:通过内核与用户空间mmap同一块内存来实现

struct epoll_event {

uint32_t events; /* Epoll events */

epoll_data_t data; /* User data variable */

};

events:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个

socket加入到EPOLL队列里

具体可以man 手册查看

深度理解select、poll和epoll码出桃花源的博客-CSDN博客select poll

epoll和select的区别菜-农的博客-CSDN博客epoll和select的区别

盘点Linux Epoll那些少为人知的缺点 - 掘金 (juejin.cn)


TCP/IP协议原理:

wireshark抓包:

需要设置连接的网卡和过滤器(端口号)

img

另外,同一台电脑不能抓自己的包

eth_ip头原理:

先看TCP/IP四层模型:

img

TCP/IP网络封包格式:

img

以太网头:(对应封包格式的前面14字节)

img

ip头:(对应封包格式的20字节)

img

IP头总共20个字节(如果带上IP Option的话,会大于20),第二行功能:将大包数据拆包,每一个数据包都有一个id身份认证、ipflags(D:没有碎片化 M:更多碎片化)、碎片化的偏移量,传输时先将大包数据拆包,再按照id和碎片化属性进行排序组装

TCP头:

TCP是一种面向连接的可靠传输

img

TCP也不一定是20个字节(有TCP可选项),其中offset指示用户数据的起始位置,各部分名称已经在图中标出

TCP的可靠传输:

1.TCP把所有要发送的数据进行编号(一个字节一个号),保证数据有序

2.发送时从当前数据位置,发送window大小的数据

如下图所示,如果SYN(发送序列号)=999,指示目前已经发送到第999号字节的数据,此时如果ACK位为1000,则表示前面999号字节都已接收到,发送方请从1000号字节开始分配,此时数据窗会滑动至1000号字节处开始发送;当然也可能ACK=800,说明上次发送的数据只接收到799号字节之前的数据,800到999号字节丢失没收到,请重新发送(一般不会出现这种情况,旨在说明其传输的可靠性),数据窗回调

img

TCP面向连接:(三次握手、四次握手)

为了便于理解原理,请参考抓包过程数据分析:

TCP_package_demo.pcapng

img

过程:

首先像之前写的网络编程程序一样,服务器先创建一个套接字(socket),绑定本机地址和端口号(bind),设置监听fd(listen)用于监听是否由客户端要连接,如果监听到,accept就会从阻塞中返回,生成一个对等套接字newfd用于和客户端交互数据(网络通信接口newfd)(为什么要绑定地址和端口呢?个人理解为绑定了地址和端口之后,便于监听客户端那边的套接字,客户端那边要connect会将服务器的地址和端口发过来,绑定了之后就相当于一块磁铁,感应到之后就会通知accept)

同样客户端那边也会创建一个套接字,用于通信

①:连接(三次握手):【此时标志位为SYN和ACK】

1.由客户端发起,第一次发起SYN=J(序列号),这里的序列号就是前面讲的每一个数据按字节编号

2.服务器回应,ACK =J+1(确认序列号),SYN=K,指示下一次客户端要发的序列号为J+1,同时发送自己的序列号K

3.客户端应答,ACK=K+1,指示服务器下一次要发送的序列号K+1

.

.

.

②:关闭(四次握手)【此时标志位为FIN和ACK】

1.客户端那边先输入quit退出,此时服务器仍能正常读取并返回一个quit字符串(可参考代码)

2.客户端从循环跳出,进行close(fd)操作,同时会将FIN标志位使能,发送一个序列号给服务器(FIN=m),同时read返回0

3.此后服务器做出应答

4.服务器也发送一个终止标志位的序列号(FIN=n)

5.客户端做出应答,服务器关闭之前用于和这个客户端通信的套接字

同时关闭后,内核会释放socket资源

还有之前代码中的地址快速重用,如果客户端退出后,服务器立即按ctrl+c,客户端并未来得及做出分手应答,会有一个等待时间服务器才会可以重用(TIME_WAIT一般为30-120s)

三次、四次握手注意点:

\1. 一定标注客户端和服务器

\2. 三次握手的连接必须是由客户端发起(四次握手客户端和服务器都可以发起)

3.SYN, ACK,FIN等标志符号应该写上

扩展:

tcp提供可靠传输的工作原理和实现过程

TCP提供可靠传输的工作原理和实现过程_tcp传输的原理和过程-CSDN博客

tcp三次握手、四次握手全过程

https://www.cnblogs.com/java1024/p/15009814.html

zhuanlan.zhihu.com/p/40013850

TIME_WAIT时间:

TCP的TIME_WAIT状态 - 知乎


网络扩展:

网络信息检索:

img

本节主要介绍域名解析函数gethostbyname():

img

struct hostent结构体:

img

img

返回值:

img

注意:

1.成员h_addr_list是二级指针类型

2.调用时要包含头文件netdb.h

3.错误号为h_errno,并非常用的errno,所以打印错误信息时,要使用herror()

4.用完该结构体,最后记得使用endhostent()将其释放,并将hs结构体指针置空

说明:

IPv4中使用gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。

代码:

img

注意hs指向结构体,该结构体中的成员h_addr_list为二级指针(指向指针的指针),而从上文可知,h_addr_list实际为数组指针(数组指针本身为二级指针),而且该数组指针指向的数组中的每一个元素都是一个32位的网络字节序ip地址,因此可以将该指针先转为uint32_t *类型的整型指针,再对其取值操作,赋给sin.sin_addr.s_addr(他要求的也是32位网络字节序形式的)

img

网络属性设置;

SYNOPSIS

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,

void *optval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int optname,

const void *optval, socklen_t optlen);

level指定控制套接字的层次.可以取三种值:

1)SOL_SOCKET:通用套接字选项.(应用层)

2)IPPROTO_TCP:TCP选项. (传输层)

3)IPPROTO_IP:IP选项. (网络层)

img

具体可man 7 socket查看

另外对于接收超时和发送超时:

struct timeval {

time_t tv_sec; /* seconds */

suseconds_t tv_usec; /* microseconds */

};

举例:

/允许绑定地址快速重用/

int b_reuse = 1;

setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,(void *)&b_reuse,sizeof(int));

/允许广播/

int b_broad = 1;

setsockopt(fd,SOL_SOCKET,SO_BROADCAST,(void *)&b_broad,sizeof(int));

/设置接收超时/

struct timeval timeout;

timeout.tv_sec = 5;

timeout.tv_usec = 0;

setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,(void *)&timeout,sizeof(struct timeval));

代码:

网络扩展.zip

网络超时优化

方法一:设置socket的属性 SO_RCVTIMEO

设置socket的属性 SO_RCVTIMEO参考代码如下 struct timeval tv; tv.tv_sec = 5; // 设置5秒时间 tv.tv_usec = 0; setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO, &tv, sizeof(tv)); // 设置接收超时 recv() / recvfrom() // 从socket读取数据

方法二:用select检测socket是否’ready’

参考代码如下 struct fd_set rdfs; while(1) { struct timeval tv = {5 , 0}; // 设置5秒时间 FD_ZERO(&rdfs); FD_SET(sockfd, &rdfs); if (select(sockfd+1, &rdfs, NULL, NULL, &tv) > 0) // socket就绪 { recv() / recvfrom() // 从socket读取数据 }}

方法三: 设置定时器(timer), 捕捉SIGALRM信号

参考代码如下 void handler(int signo) { return; } struct sigaction act; sigaction(SIGALRM, NULL, &act); act.sa_handler = handler; act.sa_flags &= ~SA_RESTART; //清除掉SIGALRM信号的SA_RESTART sigaction(SIGALRM, &act, NULL); alarm(5); if (recv(,,,) < 0) ……

心跳检测:

方法一: 数据交互双方隔一段时间,一方发送一点数据到对方,对方给出特定的应答。如超过设定次数大小的时间内还是没有应答,这时候认为异常

方法2:改变套接字的属性来实现

函数定义: 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));}~ 使用: int keepAlive = 1; //设定KeepAlive int keepIdle = 5; //开始首次KeepAlive探测前的TCP空闭时间 int keepInterval = 5; //两次KeepAlive探测间的时间间隔 int keepCount = 3; //判定断开前的KeepAlive探测次数 setKeepAlive(newfd, keepAlive, keepIdle, keepInterval, keepCount);

一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多且稍复杂,而利用TCP/IP协议层为内置的KeepAlive功能来实现心跳功能则简单得多。

不论是服务端还是客户端,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包,而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。 因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启KeepAlive功能,要用setsockopt()函数将SOL_SOCKET.SO_KEEPALIVE设置为1才是打开。(尽管这微不足道,但在按流量计费的环境下增加了费用),另一方面,KeepAlive设置不合理时可能会 因为短暂的网络波动而断开健康的TCP连接

TCP心跳检测:

TCP心跳检测-CSDN博客

TCP长连接、短连接(心跳检测)

https://juejin.cn/post/6871081474003746830


网络扩展下:

广播、多播、UNIX域套接字

广播:

* 前面介绍的数据包发送方式只有一个接受方,称为单播

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

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

* 广播地址

* 以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址

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

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

广播发送:

* 创建用户数据报套接字

* 缺省创建的套接字不允许广播数据包,需要设置属性

* setsockopt可以设置套接字属性

* 接收方地址指定为广播地址

* 指定端口信息

* 发送数据包

* int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);

* 头文件:<sys/socket.h>

* level : 选项级别(例如SOL_SOCKET)

* optname : 选项名(例如SO_BROADCAST)

* optval : 存放选项值的缓冲区的地址

* optlen : 缓冲区长度

* 返回值:成功返回0 失败返回-1并设置errno

广播接收:

* 创建用户数据报套接字

* 绑定本机IP地址和端口

* 绑定的端口必须和发送方指定的端口相同

* 等待接收数据

==sender==

#include "broadcast-net.h"


void usage(char *s)

{

​    printf("\n the following is broadcast-usage!");

​    printf("\n ./recver:%s",s);

​    printf("\n serv_ip : broadcast address");

​    printf("\n serv_port : udp server port(>5000)\n\n");

​    

}


int main(int argc,char *argv[])

{

​    int fd;

​    unsigned short port;

​    struct sockaddr_in sin;

​    char buf[BUFSIZ];


​    if(argc != 3)

​    {

​        usage(argv[0]);

​        exit(1);

​    }

​    if((port = (unsigned short)atoi(argv[2])) < 5000)

​    {

​        usage(argv[0]);

​        exit(1);

​    }


​    if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    /*允许广播*/

​    int b_br = 1;

​    setsockopt(fd,SOL_SOCKET,SO_BROADCAST,(void *)&b_br,sizeof(int));


​    bzero(&sin,sizeof(sin));


​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(port);

​    if((inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }


​    printf("sender is starting.....ok\n");


​    while(1)

​    {

​        bzero(buf,BUFSIZ);


//        printf(">");

​        fprintf(stderr,"please input a string\n");        


​        if(!(fgets(buf,BUFSIZ-1,stdin)))

​        {

​            perror("fgets");

​            continue;

​        }


​        if((sendto(fd,(void *)buf,strlen(buf),0,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​        {

​            perror("sendto");

​            break;

​        }


​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("sender(fd=%d) is exiting!\n",fd);

​            break;

​        }

​    }

​    close(fd);

​    return 0;

}

==recver==

#include "broadcast-net.h"



int main()

{

​    int fd;

​    struct sockaddr_in sin;
​    

​    /* socket fd*/

​    if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    /*允许绑定地址快速重用*/

​    int b_reuse = 1;


​    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));

​    

​    /* 绑定*/

​    /* 填充struct socket_addr_in 结构体变量*/



​    bzero(&sin,sizeof(sin));

​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);



#if 1    

​    /*优化1 使服务器程序能够在任意的IP地址上运行*/


​    sin.sin_addr.s_addr = htonl(INADDR_ANY);

#else

​    if((inet_pton(AF_INET,SERV_ADDR,(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }

#endif

​    /* 绑定*/


​    if((bind(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("bind");

​    }


​    char buf[BUFSIZ];

​    char ipv4_addr[16];

​    struct sockaddr_in cin;

​    socklen_t addrlen = sizeof(cin);


​    printf("broadcast recver is starting....ok\n");


​    while(1)

​    {

​        bzero(buf,BUFSIZ);

​        

​        if((recvfrom(fd,buf,BUFSIZ-1,0,(struct sockaddr *)&cin,&addrlen)) < 0)

​        {

​            perror("recvfrom");

​            continue;

​        }

​        

​        if(!inet_ntop(AF_INET,(void *)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin)))

​        {

​            perror("inet_ntop");

​            break;

​        }


​        printf("received broadcast data:%s\n",buf);


​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("sender(%s:%d) is exiting!\n",ipv4_addr,(int)ntohs(cin.sin_port));

​        }

​    }

​    close(fd);

​    return 0;

}

组播:

单播方式只能发给一个接收方

广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信

组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据

* 多播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)

网络地址:

img

组播是一个人发送,加入到多播组的人接受数据

组播的IP地址: 224.0.0.1~239.255.255.254(中间除掉广播)

组播必须基于UDP的编程方法

img

==multicast-sender==

#include "multicast-net.h"



void usage(char *s)

{

​    printf("\n the following is udp-usage!");

​    printf("\n ./udp-client:%s",s);

​    printf("\n serv_ip : udp serer ip address(224~239)");

​    printf("\n serv_port : udp server port(>5000)\n\n");

}


int main(int argc,char *argv[])

{

​    int fd;

​    unsigned short port;

​    struct sockaddr_in sin;

​    char buf[BUFSIZ];





​    if(argc != 3)

​    {

​        usage(argv[0]);

​        exit(1);

​    }



​    if((port = (unsigned short)atoi(argv[2])) < 5000)

​    {

​        usage(argv[0]);

​        exit(1);

​    }

​    if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    bzero(&sin,sizeof(sin));


​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(port);

​    if((inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }


​    printf("multicast sender is starting.....ok\n");



​    while(1)

​    {

​        bzero(buf,BUFSIZ);


//        printf(">");

​        fprintf(stderr,"please input a string\n");        



​        if(!(fgets(buf,BUFSIZ-1,stdin)))

​        {

​            perror("fgets");

​            continue;

​        }


​        if((sendto(fd,(void *)buf,strlen(buf),0,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​        {

​            perror("sendto");

​            break;

​        }

​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("sender(fd=%d) is exiting!\n",fd);

​            break;

​        }

​    }

​    close(fd);

​    return 0;

}

==multicast-recver.c==

#include "multicast-net.h"


int main()

{

​    int fd;

​    struct sockaddr_in sin;



​    /* socket fd*/

​    if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    /*允许绑定地址快速重用*/

​    int b_reuse = 1;


​    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));

​    

​    /*加入多播组*/

​    struct ip_mreq mreq;

​    bzero(&mreq,sizeof(mreq));


​    mreq.imr_multiaddr.s_addr = inet_addr(MULTI_ADDR);

​    mreq.imr_interface.s_addr = htonl(INADDR_ANY);

​    setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));


​    /* 绑定*/

​    /* 填充struct socket_addr_in 结构体变量*/


​    bzero(&sin,sizeof(sin));

​    sin.sin_family = AF_INET;

​    sin.sin_port = htons(SERV_PORT);


#if 1    

​    /*优化1 使服务器程序能够在任意的IP地址上运行*/


​    sin.sin_addr.s_addr = htonl(INADDR_ANY);

#else

​    if((inet_pton(AF_INET,SERV_ADDR,(void *)&sin.sin_addr.s_addr)) != 1)

​    {

​        perror("inet_pton");

​        exit(1);

​    }

#endif

​    /* 绑定*/



​    if((bind(fd,(struct sockaddr *)&sin,sizeof(sin))) < 0)

​    {

​        perror("bind");

​    }



​    char buf[BUFSIZ];

​    char ipv4_addr[16];

​    struct sockaddr_in cin;

​    socklen_t addrlen = sizeof(cin);


​    printf("udp-server is starting....ok\n");


​    while(1)

​    {

​        bzero(buf,BUFSIZ);

​        
​        if((recvfrom(fd,buf,BUFSIZ-1,0,(struct sockaddr *)&cin,&addrlen)) < 0)

​        {

​            perror("recvfrom");

​            continue;

​        }

​        
​        if(!inet_ntop(AF_INET,(void *)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin)))

​        {

​            perror("inet_ntop");

​            break;

​        }


​        printf("received from client(%s:%d),data:%s\n",ipv4_addr,(int)ntohs(cin.sin_port),buf);


​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("client(%s:%d) is exiting!\n",ipv4_addr,(int)ntohs(cin.sin_port));

​        }

​    }

​    close(fd);

​    return 0;

}

UNIX域套接字(unix domain)

只能用于本地进程间的通信

进程间通信:

1.进程间的数据共享:

管道、消息队列、共享内存、unix域套接字

易用性: 消息队列 > unix域套接字 >管道 > 共享内存(经常要和信号量一起用)

效率: 共享内存 > unix域套接字 >管道 > 消息队列

常用:共享内存、unix域套接字

\2. 异步通信:

信号

\3. 同步和互斥(做资源保护)

信号量

img

Address format

A UNIX domain socket address is represented in the following structure:

#define UNIX_PATH_MAX 108

struct sockaddr_un {

sa_family_t sun_family; /* AF_UNIX */

char sun_path[UNIX_PATH_MAX]; /* pathname */

};

* 本地地址结构

struct sockaddr_un // <sys/un.h>

{

sa_family_t sun_family;

char sun_path[108]; // 套接字文件的路径 //不同系统大小不同,一般在96-108之间

};

* 填充地址结构

struct sockaddr_un myaddr;

bzero(&myaddr, sizeof(myaddr));

myaddr.sun_family = AF_UNIX;

strcpy(myaddr.sun_path, “/tmp/mysocket”);

img

另外:

判断文件是否存在用access函数:

SYNOPSIS

#include <unistd.h>

int access(const char *pathname, int mode);

RETURN VALUE

On success (all requested permissions granted, or mode is F_OK and the

file exists), zero is returned. On error (at least one bit in mode

asked for a permission that is denied, or mode is F_OK and the file

does not exist, or some other error occurred), -1 is returned, and

errno is set appropriately.

如果文件存在就删除,删除用unlink函数

SYNOPSIS

#include <unistd.h>

int unlink(const char *pathname);

RETURN VALUE

On success, zero is returned. On error, -1 is returned, and errno is

set appropriately.

linux@linux:~/net/unix$ ls -a -l /tmp/my_domain_file.1

srwxrwxr-x 1 linux linux 0 Nov 7 20:08 /tmp/my_domain_file.1

linux七种文件类型?

LINUX中的七种文件类型

d 目录文件。

l 符号链接(指向另一个文件,类似于window下的快捷方式);

s 套接字文件;

b 块设备文件,二进制文件;

c 字符设备文件;

p 命名管道文件;

- 普通文件。

/tmp/my_domain_file.1属于内存中临时创建的文件

代码:

==unix-server.c==

#include "unix-net.h"


void cli_data_handler(void *arg);

void child_handler(int sig);


int main()

{

​    int fd;

​    /*回收子进程*/

​    

​    struct sigaction act;

​    act.sa_handler = child_handler;

​    act.sa_flags = 0;

​    sigemptyset(&act.sa_mask);


​    if((sigaction(SIGCHLD,&act,NULL)) < 0)

​    {

​        perror("sigaction");

​        exit(1);

​    }

​    /* socket fd*/

​    if((fd = socket(AF_UNIX,SOCK_STREAM,0)) < 0)//注意创建套接字时,地址协议族为本地通信

​    {

​        perror("socket");

​        exit(1);

​    }

​    /*允许绑定地址快速重用*/

​    int b_reuse = 1;


​    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));


​    /*填充本地通信地址结构体*/

​    struct sockaddr_un sun;


​    bzero(&sun,sizeof(sun));

​    sun.sun_family = AF_UNIX;


​    if(!access(UNIX_DOMAIN_FILE,F_OK)) //判断文件是否存在,存在将其删除

​    {

​        unlink(UNIX_DOMAIN_FILE);

​    }


​    strcpy(sun.sun_path,UNIX_DOMAIN_FILE); //拷贝至sun_path,重新创建文件


​    /* 绑定*/



​    if((bind(fd,(struct sockaddr *)&sun,sizeof(sun))) < 0)

​    {

​        perror("bind");

​    }


​    /*调用listen把主动套接字转为被动套接字*/


​    if((listen(fd,BACKLOG)) < 0)

​    {

​        perror("listen");

​        exit(1);

​    }

​    printf("unix server is starting....ok\n");


​    /*阻塞等待客户端连接请求*/



​    /*优化3: 用多进程或多线程处理已经建立好连接的客户端数据*/


​    int newfd;

​    pid_t pid;


​    while(1)

​    {

​        if((newfd = accept(fd,NULL,NULL)) < 0)

​        {

​            if(newfd < 0 && EINTR == errno)

​            {

​                continue;

​            }

​            else

​            {

​                perror("accept");

​                exit(1);

​            }

​        }


​        /*创建一个子进程用于处理已经建立连接的客户端的交互数据*/


​        if((pid = fork()) < 0)

​        {

​            perror("fork");

​            break;

​        }

​        else if(0 == pid)

​        {
​            close(fd);

​            printf("client is connected\n");

​            cli_data_handler(&newfd);

​            return 0;

​        }

​        else

​        {

​            close(newfd);

​        }

​    }


​    close(fd);

​    return 0;

}


void cli_data_handler(void *arg)

{

​    int newfd = *(int *)arg;

​    printf("child-process: newfd=%d\n",newfd);


​    /*与newfd进行数据读写*/

​    char buf[BUFSIZ];

​    int ret;

​    char resp_buf[BUFSIZ+10];


​    while(1)

​    {
​        bzero(buf,BUFSIZ);

​        do

​        {

​            ret = read(newfd,buf,BUFSIZ-1);

​        }while(ret < 0 && EINTR == errno);


​        if(ret < 0)

​        {

​            perror("read");

​            exit(1);

​        }


​        if(!ret)//对方已经关闭

​        {

​            break;

​        }

​        printf("read data:%s\n",buf);


​        bzero(resp_buf,BUFSIZ+10);


​        strncpy(resp_buf,SERV_RESP,strlen(SERV_RESP));

​        strcat(resp_buf,buf);


​        do

​        {

​            ret = write(newfd,resp_buf,strlen(resp_buf));

​        }while(ret < 0 && EINTR == errno);


​        if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​        {

​            printf("client(%d) is exiting\n",newfd);

​            break;

​        }

​    }

​    close(newfd);

}


void child_handler(int sig)

{

​    if(SIGCHLD == sig)

​    {

​        waitpid(-1,NULL,WNOHANG);

​    }

}

==unix-client.c==

#include "unix-net.h"


void usage(char *s)

{
​    printf("\n %s UNIX_DOMAIN_FILE\n\n",s);

}

int main(int argc,char **argv)

{

​    int fd;

​    struct sockaddr_un sun;

​    char buf[BUFSIZ];

​    if(argc != 2)

​    {

​        usage(argv[0]);

​        exit(1);

​    }

​    if((fd = socket(AF_UNIX,SOCK_STREAM,0)) <0)

​    {

​        perror("socket");

​        exit(1);

​    }


​    bzero(&sun,sizeof(sun));


​    sun.sun_family = AF_UNIX;

​    
​    /*确保UNIX_DOMAIN_FILE文件存在且可写*/

​    if((access(UNIX_DOMAIN_FILE,F_OK | W_OK)) < 0)

​    {

​        exit(1);

​    }


​    strcpy(sun.sun_path,UNIX_DOMAIN_FILE);


​    if((connect(fd,(struct sockaddr *)&sun,sizeof(sun))) < 0)

​    {

​        perror("connect");

​        exit(1);

​    }

​    printf("unix-client is starting...ok\n");


​    fd_set rset;

​    int maxfd;

​    struct timeval timeout;

​    int ret;


​    while(1)

​    {

​        FD_ZERO(&rset);

​        FD_SET(0,&rset);

​        FD_SET(fd,&rset);

​        maxfd = fd;

​        timeout.tv_sec = 5;

​        timeout.tv_usec = 0;


​        select(maxfd+1,&rset,NULL,NULL,&timeout);


​        if(FD_ISSET(0,&rset)) //标准键盘有输入,读取标准键盘的数据写到套接字里

​        {

​            bzero(buf,BUFSIZ);

​            do

​            {

​                ret = read(0,buf,BUFSIZ-1);

​            }while(ret < 0 && EINTR == errno);


​            if(ret < 0)

​            {

​                perror("read from stdin");

​                continue;

​            }


​            else if(!ret)

​            {

​                continue;

​            }


​            else

​            {

​                if((write(fd,buf,strlen(buf))) < 0)

​                {

​                    perror("write to socket");

​                    continue;

​                }

​            }


​            if(!strncasecmp(buf,QUIT,strlen(QUIT)))

​            {

​                printf("client(fd=%d) is exiting\n",fd);



​                break;

​            }

​        }


​        if(FD_ISSET(fd,&rset))  //服务器那边有数据过来,读取数据

​        {

​            bzero(buf,BUFSIZ);

​            do

​            {

​                ret = read(fd,buf,BUFSIZ-1);

​            }while(ret < 0 && EINTR == errno);


​            if(ret < 0)

​            {

​                perror("read from socket");

​                continue;

​            }


​            else if(!ret) //服务器关闭

​            {

​                break;

​            }


​            else

​            {

​                printf("server said:%s\n",buf);

​                /*this is a bug */

​                if((strlen(buf) > strlen(SERV_RESP)) && (!strncasecmp(buf+strlen(SERV_RESP),QUIT,strlen(QUIT))))

​                {
​                    printf(" server client is exiting\n");

​                    break;

​                }

​            }

​        }

​    }

​    close(fd);

​    return 0;

}

img

如图所示,server先创建一个内存临时文件(创建前先判断存在不存在,存在先删除再创建,该文件绑定在套接字上),client判断文件是否存在且可写,若是,则可以文件中写入,server读取数据

作业:

1.【不定项选择】 以下功能用到UDP协议实现的是 ( )

A. 广播

B. 组播

C.带外数据

D.ping

A,B C. 带外数据 带外数据,(也称为TCP紧急数据),会用到TCP,肯定是用不到UDP啦。 D. ping ping命令是基于IP层(网络层)的 ICMP 协议,不会用到UDP;

拓展:

1.一个主机可能有多个接口

2.sudo lsof -i查端口号

3.虚拟机联网的三种模式:

VMware虚拟机三种网络模式:桥接模式,NAT模式,仅主机模式_虚拟机桥接模式是什么意思-CSDN博客

4.局域网和公网:

1、局域网

局域网(Local Area Network),简称LAN,是指在某一区域内由多台计算机互联成的计算机组。“某一区域”指的是同一办公室、同一建筑物、同一公司和同一学校等,一般是方圆几千米以内。局域网可以实现文件管理、应用软件共享、打印机共享、扫描仪共享、工作组内的日程安排、电子邮件和传真通信服务等功能。局域网是封闭型的,可以由办公室内的两台计算机组成,也可以由一个公司内的上千台计算机组成。

2:内网

内网就是局域网,网吧、校园网、单位办公网都属于此类。另外光纤到楼、小区宽带、教育网、有线电视Cable Modem上网虽然地域范围比较大但本质上还是基于以太网技术,所以任然属于内网。

3:公网

公网就是普通电路交换网,即现在的网通,电信,铁通等架设的骨干及分支网络。

详细信息请参考;

https://www.baidu.com/link?

5.!g 调出以g开头的最后一个命令

6.listen的作用?

第一篇:

根据man listen得到的解释如下:

backlog参数定义了存放pending状态(挂起、护着搁置)的连接的队列的最大长度;如果在队列满的时候,一个连接请求到达,客户端可能会收到一个错误:ECONREFUSED。

然后man listen的下面有一个小提示:

现在backlog这个参数指示的是存放已经建立连接(established)并等待被accept的sockets的队列的长度。

没有完成的socket队列的长度可以通过/proc/sys/net/ipv4/tcp_max_syn_backlog这个参数来设置。

如果backlog参数大于 /proc/sys/net/core/somaxconn 的值,那么该值将被自动截断为somaxconn的值,它的值默认是128。

https://blog.csdn.net/weixin_33857230/article/details/86234721

第二篇更为详细:

img

img

linux tcp socket 请求队列大小参数 backlog 简介_tcpbacklog一般多大-CSDN博客

还可参考:

再理解tcp backlog-CSDN博客

7.getsockname

用于返回与某个套接字关联的套接字协议地址

https://www.baidu.com/link?

8.socke套接字

注意:socket套接字的通信结构如下,客户端与服务器分别各有一个发送端、接收端

全双工通信,既可以发送又能同时接收,所以才会出现程序里调用write将缓冲区写进套接字,同时紧接着又能读取(read)的现象。

img

9.nc 命令

腾讯云 产业智变·云启未来 - 腾讯

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值