网络编程/计算机网络

目录

一、网络基本概念

1.网络

2.互联网

3.ip地址

4.MAC地址

5.端口号Port

6.网络协议

二.网络分层模型

1.物理层

2.数据链路层

3.网络层

4.传输层

5.应用层

三、三次握手与四次挥手

1.TCP头部

2.三次握手与四次挥手

四、网络应用程序通信流程

五、网络编程

1.主机字节序列和网络字节序列

2.网络应用编程接口——套接字

基本概念     

套接字地址结构

(1)通用 socket 地址结构

 (2)专用 socket 地址结构

(3)IP 地址转换函数

3.网络编程接口

4.TCP 编程流程

单次连接发送数据

循环发送数据

多线程处理并发

多进程处理并发

5.UDP编程流程


一、网络基本概念

1.网络

网络:由若干结点和连接这些结点的链路组成,网络中的结点可以是计算机,交换机、 路由器等设备。

网络设备有:交换机、路由器、集线器

传输介质有:双绞线、同轴电缆、光纤

一个简单的网络示意图

2.互联网

把多个网络连接起来就构成了互联网。目前最大的互联网就是我们常说的因特网。

3.ip地址

        IP 地址就是给因特网上的每一个主机(或路由器)的每一个接口分配的一个在全世界 范围内唯一的标识符。IP 地址因其特殊的结构使我们可以在因特网上很方便地进行寻址。

        IP地址的目的:资源共享、信息交互

        IP 地址有分 IPV4 和 IPV6 两种类别格式,IPV4 是类似”A.B.C.D”的格式,它是 32 位 的,用“.”分成四个段,每个段是 8 个位(值为 0-255),用 10 进制表示。IPV6 地址是 128 位,格式类似”XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX”,用“:“分成 8 个 段,每个段 16 个位,用 4 个 16 进制数表示。

  • IPV4 地址示例: “192.168.31.1”。ipv4位数:32。
  • IPV6 地址示例:“2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b”。ipv6位数:128。
  • IP分为网络号+主机号
    • 范围:(给一个ip地址问是哪一类)
  • 查看ip命令—— windows:ipconfig ;Linux:ifconfig

  • 127.0.0.1 自己电脑的ip,用于测试

  • IP地址、MAC物理地址(48位)唯一标识一台主机

4.MAC地址

        在局域网中,硬件地址又称为物理地址或者 MAC 地址,长度 48 位,是固化在计算机适配器的 ROM 中的地址。因此假定连接在局域网上的一台计算机的适配器坏了而我们更换了 一个新的适配器,那么这台计算机的局域网的“地址”也就改变了,虽然这台计算机的地理 位置没有发生变化。其次,当我们把一个笔记本从一个城市带到另一个城市时,虽然地理位置改变了,但是电脑在局域网中的“地址”仍然不变。由此可见,局域网上某个主机的“地址”根本不能告诉我们这台主机位于什么地方。在网络上方便的寻找某个主机,还得依靠 ip 地址进行。

  • MAC:固化的不变
  • IP:动态的,可以反映出加入与退出网络的变化

5.端口号Port

        一个ip地址表示一台主机。主机运行一台进程,需要和其他主机联网通信,这就依赖于端口号。

        端口可以理解为应用程序代号,是软件层次的,用于表示一个进程/应用程序,目的是方便查找,可以实现不同主机之间的通信。

        ip+port->进程 ,想要实现进程间通信需要:自己的ip+端口与对方的ip+端口

6.网络协议

网络协议就是一组网络规则的集合,是我们共同遵守的约定或标准。

常见的协议: 

  • HTTP:超文本传输协议
  • FTP: 文件传输协议
  • TELNET : 是 internet 远程登陆服务的标准协议。
  • TCP : 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可 靠的、基于字节流的传输层通信协议
  • UDP :用户数据报协议
  • IP : Internet Protocol 简称 IP,又译为网际协议或互联网协议
  • ICMP :因特网控制报文协议 
  • ARP : 地址解析协议,是根据 IP 地址获取 MAC 地址的协议 
  • RARP : 逆地址解析协议

二.网络分层模型

(面试会考:

分层的目的?哪些层?各层次功能?

OSI 的 7 层模型与 tcp/ip 协议族体系 4 层结构

1.物理层

        物理层的主要任务就是确定与传输媒体的接口相关的一些特性,如机械特性、电气特性、物理特性和过程特性

2.数据链路层

        主要功能是:通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路。 在计算机网络中由于各种干扰的存在,物理链路是不可靠的。因此,这一层的主要功能 是在物理层提供的比特流的基础上,通过差错控制,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。 该层通常又被分为介质访问控制(MAC)和逻辑链路控制(LLC)两个子层。

  • MAC 子层的主要任务是解决共享型网络中多用户对信道竞争的问题,完成网络介质的 访问控制;
  • LLC 子层的主要任务是建立和维护网络连接,执行差错校验、流量控制和链路控制。

        TCP/IP 协议体系结构中,数据链路层的功能描述为实现网卡接口的网络驱动程序,以 处理数据在物理媒介上的传输,不同的物理网络具有不同的电气特性,网络驱动程序隐藏了 这些细节,为上层协议提供了一个统一的接口。这一层主要关注的三个基本问题是:封装成帧,透明传输和差错检测。

3.网络层

        网络层实现数据包的选路和转发。广域网或者说互联网通常使用众多分级的路由器来连接分散的主机或者局域网,因此,通信的两台主机一般不是直接相连的,而是通过多个中间 结点(路由器)连接的。网络层的任务就是选择这些中间结点,以确定两台主机之间的通信路径。同时,网络层对上层协议隐藏了网络拓扑连接的细节,使得在传输层和网络应用程序看来,通信的双方是直接相连的。

         网络层最核心的协议是 IP 协议(Internet Protocol,因特网协议)。IP 协议根据数据包的目的 IP地址来决定何如投递它。如果数据包不能直接发送给目标主机,那么 IP 协议就是为它寻找一个合适的吓一跳路由器,并将数据包交付给该路由器来转发。多次重复这一过程,数据包最终到达目标主机,或者由于发送失败而被丢弃。可见,IP 协议使用逐跳的方式确定通 信路径。

        网络层另外一个重要的协议是 ICMP 协议(因特网控制报文协议)。它是 IP 协议的重要 补充,主要用于检测网络连接。

        IP 协议为上层协议提供无状态、无连接、不可靠的服务。

         无状态是指通信双方不同步传输数据的状态信息,因此所有 IP 数据报的发送、传输和 接收都是相互独立、没有上下文关系的。这种服务最大的缺点是无法处理乱序和重复的 IP 数据报。虽然 IP 数据报头部提供了一个标识字段用以唯一标识一个 IP 数据报,但它是被用 来处理 IP 分片和重组的,而不是用来指示接收顺序的。无状态的优点是简单、高效。无须 为保持通信状态而分配一些内核资源,也无须再每次通信时携带状态信息。

        无连接是指 IP 通信双方都不长久地维持对方的任何信息。这样,上层协议每次发送数 据的时候,都必须明确指定对方的 IP 地址。 不可靠是指 IP 协议不能保证 IP 数据报准确地到达接收端,它只是承诺尽最大努力。

IPV4 头部结构如下:

4.传输层

        传输层为两台主机上的应用程序提供端到端的通信。与网络层使用的逐跳通信的方式不 同,传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程。

         传输层协议主要有三个:TCP 协议、UDP 协议和 SCTP 协议为应用层提供可靠的、面向连接的和基于流的服务。

        TCP 协 议使用超时重传、确认应答等方式来确保数据包被正确的发送至目的端,因此 TCP 服务是 可靠的。使用 TCP 协议通信的双方必须先建立 TCP 连接,并在内核中为该连接维持一些必 要的数据结构,比如连接状态,读写缓冲区等。当通信结束时,双方必须关闭连接以释放这 些内核数据。TCP 服务是基于流的,基于流的数据没有边界(长度)限制,它源源不断地从 通信地一端流入另一端。发送端可以逐个字节地向数据流中写入数据,接收端可以逐个字节地将它们读出。

        UDP 协议(用户数据报协议)则与 TCP 协议完全相反,它为应用层提供不可靠、无连接、基于数据报地服务。“不可靠”意味着 UDP 协议无法保证数据从发送端正确地传送到目 的端。如果数据在中途丢失,或者目的端通过数据校验发现数据错误而将其丢弃,则 UDP 协议只是简单地通知应用程序发送失败。因此,如果要使 UDP 协议可靠,那么应用程序通 常要自己处理数据确认、超时重传等逻辑。UDP 是无连接的,即通信双发不保持一个长久 的联系,因此应用程序每次发送数据都要明确指定 接收端的地址。基于数据报的服务,是相 对基于流的服务而言的。每次 UDP 数据报都有一个长度,接收端必须以该长度为最小单位 将其所有内容一次性读出,否则数据将被截断。

        SCTP 协议(流控制传输协议)是一种相对较新的传输层协议,它是为了再因特网上传 输电话信号而设计的。这里暂时不讨论 SCTP 协议。

5.应用层

        负责处理应用程序的逻辑。

三、三次握手与四次挥手

1.TCP头部

TCP:面向连接,通讯前进行三次握手建立连接,是可靠的流式服务。

2.三次握手与四次挥手

三次握手完成,链接才建立,才能send发送数据

i:32位序号

ACK:确认信息:i+1

四次挥手之后,链接断开。执行close之后,才会发送FIN

四次挥手也可以是三次挥手:在客户端close()后,服务器端立马收到也close,此时就是三次挥手

  • 问:哪一行代码结束后开始进行三次握手?
  • 答:connect()之后

四、网络应用程序通信流程

        如下图,应用程序 A 要将数据”hello” 传给网络上另外一台主机上的应用程序 B。

  • 数据“hello”从应用层发送给传输层
  • 传输层在数据前面加上 tcp 协议或 udp 协议的报头, 将整条报文发给网络层
  • 网络层添加自己的 IP 报头,再将整条数据发送给数据链路层。
  • 数据链路层将数据封装成能在网络中独立传输的数据单元,即数据帧。
  • 封装好的数据帧通过网络传输到另一台主机,然后再从下层依次拆包,将数据部分送往应用层。
  • 最后,应用程序 B就得到 了数据”hello”。

五、网络编程

1.主机字节序列和网络字节序列

  • 主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。
  • 大 端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址 处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的 低地址处。
  • 在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
  • Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:
  • uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序
    uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序
    uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序
    uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序

2.网络应用编程接口——套接字

基本概念     

   应用程序编程接口也称为系统调用接口,是应用程序直接调用操作系统的功能为自己服务的机制。大多数操作系统都是通过应用程序编程接口来支持应用程序运行的。

        通过应用程序编程接口调用操作系统功能被称为系统调用。虽然系统调用与一般程序中的函数调用非常相似,只不过系统调用是将控制权传递给操作系统。当某个应用进程启动系统调用时,控制权就从应用程序传递给系统调用接口。

        套接字编程接口给出了应用程序能够调用的一组函数——socket函数,每个函数完成一种与协议软件交互的一种操作,比如,用来创建套接字的socket函数,用来发送数据的end函数,用来和网络上接受数据的receive函数等。应用程序通过调用这些函数就可以达到利用网络进行通信的目的。

        简单的说,套接字使用用程序调用网络协议进行通信的接口,具有通过网络收发数据的能力。

操作系统提供三种不同的套接字:

  • 流式套接字stream socket:
    • 提供面向连接可靠的双向数据传输服务,通信双方可以实现无差错、无重复的发送和接收数据,并且保证接收端按发送顺序接收数据;
    • 使用TCP协议实现数据传输。
  • 数据报套接字datagram socket:
    • 提供无连接的、不可靠的数据传输服务,每个数据包都独立的发送,不提供无错保证,数据可能丢失,并且接收端不一定按照发送顺序接收数据包;
    • 但是用数据报套接字编程较为简单且效率高,在出错率较低的网络环境下使用的应用或对数据可靠性要求不是很高的应用通常采用该类套接字。
    • 使用UDP协议实现数据通信。
  • 原始套接字raw socket:
    • 允许对IP、ICMP等较低层协议直接访问。
    • 用于检验新的协议实现或访问现有服务中配置的新设备。

套接字地址结构

(1)通用 socket 地址结构

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr
{
    sa_family_t sa_family;
    char sa_data[14];
};ket.h>

        sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对 应。常见的协议族和对应的地址族如下图所示:

 (2)专用 socket 地址结构

        TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分 别用于 IPV4 和 IPV6:

/*
sin_family: 地址族 AF_INET
sin_port: 端口号,需要用网络字节序表示
sin_addr: IPV4 地址结构:s_addr 以网络字节序表示 IPV4 地址
*/

struct in_addr
{
    u_int32_t s_addr;
};

struct sockaddr_in //bind的第二个参数类型
{
    sa_family_t sin_family;//用于存放代表不同协议族地址的代码,internet协议族代码为AF_INET
    u_int16_t sin_port;//用于存放地址通信使用的16位端口号,以网络字节顺序表示
    struct in_addr sin_addr;//存放网络字节顺序表示的32位IP地址
//将ip地址设置为INADDR_ANY表示将该套接字的IP地址由系统自己指定
//如果希望系统自动分配端口号,就将port定义为0即可
};

struct in6_addr
{
    unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示
};

struct sockaddr_in6
{
    sa_family_t sin6_family; // 地址族:AF_INET6
    u_inet16_t sin6_port; // 端口号:用网络字节序表示
    u_int32_t sin6_flowinfo; // 流信息,应设置为 0
    struct in6_addr sin6_addr; // IPV6 地址结构体
    u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
};

(3)IP 地址转换函数

        通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化 为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表 示的 IPV4 地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网络字节序
int inet_aton(const char*cp,struct in_addr*inp);
char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示

inet_addr

作用:将点分十进制字符串表示的IP地址转换为32位无符号长整形数

inet_aton

将点分十进制的Ip地址转换为32位无符号长整形,并存放在参数inp指向的in_addr结构变量中。

inet_ntoa

将一个包含在inet_addr结构变量中的长整型ip地址转换为点分十进制形式

3.网络编程接口

头文件

1. #include <sys/types.h>
2. #include <sys/socket.h>

socket

 int socket(int domain, int type, int protocol);
  • 作用:创建套接字
  • 参数:
    • 第一个参数domain用于设置套接字的协议簇, AF_UNIX, AF_INET, AF_INET6,通常取值为表示TCP/IP协议地址族的常量AF_INET。
    • 第二个参数 type用于设置套接字的服务类型:SOCK_STREAM表示流式套接字, SOCK_DGRAM表示数据包套接字,SOCK_RAW表示原始套接字。
    • 第三个参数protocol一般设置为 0,表示使用默认协议
  • 返回值:成功返回套接字的文件描述符,失败返回-1

注意,服务器程序和客户程序创建套接字的用处不同,服务器创建的套接字是监听套接字,并不用于数据收发。客户端创建的套接字则有两个用途:一是向服务器发送连接建立请求,二是用于进行客户端的数据收发。

bind

socket()函数在创建套接字时并没有为创建的套接字分配地址,因此,服务器软件在创建了监听套接字后,还必须为它分配一个地址,该地址为套接字指定需要监听的端口号和IP地址。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:将 sockfd 与一个 socket 地址绑定
  • 函数参数:
    • 第一个参数 sockfd 是网络套接字描述符。
    • 第二个参数addr 是结构指针,该结构中包含了要绑定的地址和端口号。
    • 第三个参数addrlen 是 socket 地址的长度,确定第二个参数addr缓冲区的长度
  • 返回值:成功返回 0,失败返回-1

listen

int listen(int sockfd, int backlog);
  • 作用:创建一个监听队列以存储待处理的客户连接
  • 参数:
    • 第一个参数sockfd 是被监听的 socket 套接字。
    • 第二个参数backlog 表示处于完全连接状态的 socket 的上限,等待连接的最大队列长度。
  • 返回值:成功返回 0,失败返回-1。(一般测试时监听不会出错)

建立连接

        服务器在将监听套接字设置为监听模式后,便可用accept函数接收客户端的连接请求,客户端则在创建套接字后调用connect函数发起连接建立过程,如下“三次握手”建立连接的过程:

accept

accpet函数只适用于面向连接的套接字,而且与listen一样只能由服务器端调用。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 作用:从 listen 监听队列中接收一个连接,接收指定的监听套接字上传入的一个连接请求,并尝试与请求建立连接。
  • 参数:
    • 第一个参数sockfd 是执行过 listen 系统调用的监听 socket。
    • 第二个参数addr是结构指针,其结构体内包含一组客户端的端口号、IP地址等信息,该参数用来获取被接受连接的远端 socket 地址。
    • 第三个参数addrlen 指定该 socket 地址的长度。
  • 返回值:成功返回一个新的连接 socket,该 socket 唯一地标识了被接收的这个连接,失败返回-1

 connect

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
  • 作用:客户端需要通过此系统调用来主动与服务器建立连接。客户端调用connect函数主动发起连接,TCP协议开始三次握手过程,三次握手之后connect函数返回。
  • 参数:
    • 第一个参数sockfd 参数是由 socket()返回的一个 socket。
    • 第二个参数serv_addr 是服务器监听的 socket 地址。
    • 第三个参数addrlen 则指定这个地址的长度
  • 返回值: 成功返回 0,失败返回-1
  • 说明:
    • int n=recv(c,buff,127,0);//期望接受的个数:127
                              if(n<=0)//n=0:对方关闭,不需要再阻塞,n=-1,返回错误

      n=0是唯一判断对方关闭连接的条件

TCP 数据读写
recv()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
send()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长

ssize_t recv(int sockfd, void *buff, size_t len, int flags);
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
  • 参数: flags 参数为数据收发提供了额外的控制,通常取0,表示正常发送/接收数据。
  • 返回值:send成功返回成功发送的字节数,该字节数有可能小于len。recv则为从套接字读取buff的字节数。如果连接失败返回0。

UDP 数据读写

ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,struct sockaddr* src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t len, int flags,struct sockaddr* dest_addr, socklen_t addrlen); 
  • recvfrom()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
  • src_addr 记录发送端的 socket 地址
  • addrlen 指定该地址的长度
  • sendto()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度
  • dest_addr 指定接收数据端的 socket 地址
  • addrlen 指定该地址的长度

close

int close(int sockfd);
  • 作用:关闭一个连接,实际上就是关闭该连接对应的 socket
  • 参数:要被关闭的套接字接口描述符。
  • 返回指针:成功返回0,失败返回SOCK_ERROR错。

4.TCP 编程流程

TCP 提供的是面向连接的、可靠的、字节流服务。TCP 的服务器端和客户端编程流程如 下:

  • socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类 型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
  • bind()方法是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果 主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。
  • listen()方法是用来创建监听队列,将套接字设为处于监听状态
    • 监听队列有两种:
      • 一个是存放未完成三次握手的连接
      • 一种是存放已完成三次握手的连接。
      • 客户端三次握手完成从未完成队列到在已完成队列中添加元素;accept()在已完成队列中拿走元素。
      • listen()第二个参数n,在linux系统是已完成三次握手队列的长度。但实际能够存放的连接是n+1个。unix:已完成+未完成的。

      • 套接字也是个文件描述符(文件),一个进程只能打开1024个文件,但是可使用的大小是1021,因为0,1,2用于标准输入、标准输出和标准错误输出。
      • 问:如果监听队列内达到已完成三次握手的长度时,再有客户端连接,会出现什么情况?
        • 答:客户端会阻塞着,不会接收到任何需要recv的请求
      • 命令"ulimit -n"和"ulimit -a"可查看打开文件的数目
  • 程序如果不退出开始反复执行:
    • accept()等待客户机连接的到来,处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
    • recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数 据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
    • send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入 到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入 到发送缓冲区中的数据长度。
    •  close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
  • connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方 法执行后,会进行三次握手, 建立连接。

单次连接发送数据

  •  TCP 服务端代码 TcpServer.c :
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>

//服务器
int main()
{
	//创建套接字
	int sockfd=socket(AF_INET,SOCK_STREAM,0);//tcp:STREAM流协议		
	if(sockfd==-1)
	{
		exit(0);
	}
	 struct sockaddr_in saddr,caddr;//saddr服务器地址,caddr:客户端
	 
	 memset(&saddr,0,sizeof(saddr));//清空
	 saddr.sin_family=AF_INET;
	 saddr.sin_port=htons(6000);//端口,1024以内是知名端口,尽量自己选择较大的端口
	 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//点分十进制转换为32位无符号长整形

	 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	if(res==-1)
	{
		printf("bind failed\n");
		exit(0);
	}
    res=listen(sockfd,5);
    if(res==-1)
    {
            exit(0);
    }
	while(1)
	{
		socklen_t len=sizeof(caddr);
		int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
        //sockfd:监听套接字;c:连接套接字
		if(c<0)
		{
			continue;
		}
		printf("accept c=%d\n",c);

		char buff[128]={0};
		int n=recv(c,buff,127,0);
		printf("recv(%d):%s\n",n,buff);
		send(c,"ok",2,0);
		close(c);
	}
}

TCP 客户端代码 TCPClient.c:

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

//客户端
int main()
{
	//创建套接字
	int sockfd=socket(AF_INET,SOCK_STREAM,0);//tcp:STREAM流协议		
	if(sockfd==-1)
	{
		exit(0);
	}
	 struct sockaddr_in saddr;//服务器地址
	 //清空
	 memset(&saddr,0,sizeof(saddr));
	 saddr.sin_family=AF_INET;
	 saddr.sin_port=htons(6000);//端口
	 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//点分十进制转换为32位无符号长整形

	 int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//连接服务器
	if(res==-1)
	{
		printf("connect failed\n");
		exit(0);
	}
	printf("input:\n");
	char buff[128]={0};
	fgets(buff,128,stdin);
	send(sockfd,buff,strlen(buff)-1,0);
	memset(buff,0,128);
	recv(sockfd,buff,127,0);
	printf("buff=%s\n",buff);
	close(sockfd);
	exit(0);
}

运行结构:

循环发送数据

服务器端代码修改:

        while(1)
        {
                struct sockaddr_in caddr;
                socklen_t len=sizeof(caddr);
                int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
                if(c<0)
                {
                        continue;
                }
                printf("accept c=%d\n",c);
                //循环
                while(1)
                {
                        char buff[128]={0};
                        int n=recv(c,buff,127,0);//期望接受的个数:127
                        if(n<=0)//n=0:对方关闭,不需要再阻塞,n=-1,返回错误
                        //n=0是唯一判断对方关闭连接的条件
                        {
                                break;
                        }
                        //收到数据,打印出来并回复对象
                        printf("recv(%d):%s\n",n,buff);
                        send(c,"ok",2,0);
                        close(c);
                }
        }

代码说明:

int n=recv(c,buff,127,0);//期望接受的个数:127
if(n<=0)//n=0:对方关闭,不需要再阻塞,n=-1,返回错误
{...}

n=0是唯一判断对方关闭连接的条件

客户端代码修改:

        while(1)
        {
                printf("input:\n");
                char buff[128]={0};
                fgets(buff,128,stdin);
		        if(strncmp(buff,"end",3)==0)
	        	{
		        	break;
	        	}
                send(sockfd,buff,strlen(buff)-1,0);
                memset(buff,0,128);
                recv(sockfd,buff,127,0);
                printf("buff=%s\n",buff);
        }
        close(sockfd);
        exit(0);

运行结果:

一个思考:

问:如果接收recv一次只读一个字符,客户端send一次发送了个"hello",请问服务器端接受情况为?

答:recv输出是h一次,e一次,l一次,l一次,o一次。服务器端一次发给客户端5个ok:"okokokokok",但客户端可能会只收到一个"ok",因为在接收缓冲区中存着没发过来,下次recv发数据后,就会收到上次没收到的的4个"ok"。同样,实际客户端每次发送的数据(假设“123”和“456”,他们都会缓冲区内放着123456,读走多少拿多少,剩下的就在缓冲区内等着。

问:如何看到发送缓冲区还有多少数据为发送?(面试会问)

答:命令netstat -natp

多线程处理并发

上面程序没法实现实现同时处理两个以上的客户端,必须一个客户端退出close之后,另一个客户端的连接才能被服务器accept连接上。用线程就可以解决该问题,实现多客户端同时连接服务区。

服务器端修改代码如下:

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

//服务器
struct ArgNode
{
	int c;
};

int socket_init()
{
	//创建套接字
	int sockfd=socket(AF_INET,SOCK_STREAM,0);//tcp:STREAM流协议		
	if(sockfd==-1)
	{
		exit(0);
	}
	 struct sockaddr_in saddr,caddr;//saddr服务器地址,caddr:客户端
	 
	 memset(&saddr,0,sizeof(saddr));//清空
	 saddr.sin_family=AF_INET;
	 saddr.sin_port=htons(6000);//端口
	 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//点分十进制转换为无符号整形

	 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	if(res==-1)
	{
		printf("bind failed\n");
		exit(0);
	}
	res=listen(sockfd,5);
	if(res==-1)
	{
		exit(0);
	}
	return sockfd;
}
void *fun(void* arg)
{
	struct ArgNode*p=(struct ArgNode*)arg;
	int c=p->c;
	while(1)
	{
		char buff[128]={0};
		int n=recv(c,buff,127,0);
		if(n<=0)
		{
			break;
		}
		printf("buff(%d):%s",c,buff);
		send(c,"ok",2,0);
	}
	close(c);
	printf("cli close\n");
}
int main()
{
	int sockfd=socket_init();
	if(sockfd==-1)
	{
		exit(0);
	}

	while(1)
	{
		struct sockaddr_in caddr;
		socklen_t len=sizeof(caddr);
		int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
		if(c<0)
		{
			continue;
		}
		printf("accept c=%d\n",c);
		//启动线程
		pthread_t id;
		struct ArgNode * p=(struct ArgNode*)malloc(sizeof(struct ArgNode));
		p->c=c;
		pthread_create(&id,NULL,fun,(void*)p);
	}
	close(sockfd);
}

多进程处理并发

代码:

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

//服务器
int socket_init()
{
	//创建套接字
	int sockfd=socket(AF_INET,SOCK_STREAM,0);//tcp:STREAM流协议		
	if(sockfd==-1)
	{
		exit(0);
	}
	 struct sockaddr_in saddr,caddr;//saddr服务器地址,caddr:客户端
	 
	 memset(&saddr,0,sizeof(saddr));//清空
	 saddr.sin_family=AF_INET;
	 saddr.sin_port=htons(6000);//端口
	 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//点分十进制转换为无符号整形

	 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	if(res==-1)
	{
		printf("bind failed\n");
		exit(0);
	}
	res=listen(sockfd,5);
	if(res==-1)
	{
		exit(0);
	}
	return sockfd;
}

void recv_data(int c)
{
	while(1)
	{
		char buff[128]={0};
		int n=recv(c,buff,127,0);
		if(n<=0)
		{
			break;
		}
		printf("buff=%s\n",buff);
		send(c,"ok",2,0);
	}
	close(c);
}

int main()
{
	int sockfd=socket_init();
	if(sockfd==-1)
	{
		exit(0);
	}

	while(1)
	{
		struct sockaddr_in caddr;
		socklen_t len=sizeof(caddr);
		int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
		if(c<0)
		{
			continue;
		}
		pid_t pid=fork();
		int socket_init();
		if(pid==-1)
		{
			close(c);
			continue;
		}
		if(pid==0)
		{
			recv_data(c);
			exit(0);
		}
		close(c);
	}
}

运行结果:

注意,引入子进程,一定要注意处理僵死进程的问题,如下,通过输入“ps -ef | grep fork”命令查看当前进程情况:

   其中可看出,当一个客户端与服务器主动输入“end”或者ctrl+c断开连接时,服务器会产生一个僵死进程,下面利用信号来解决该问题:

#include<sys/wait.h>
#include<signal.h>
void wait_child(int sig)
{
        wait(NULL);
}
int main()
{
        signal(SIGCHLD,wait_child);
        int sockfd=socket_init();
        ....
}
signal(SIGCHLD,SIG_IGN);

5.UDP编程流程

服务器:创建套接字

接收客户端数据

UDP是:无连接 不可靠(发出去就不管了,成功与否不保障)效率高,实时性好

  • socket()用来创建套接字,使用 udp 协议时,选择数据报服务 SOCK_DGRAM。
  • sendto() 用来发送数据,由于 UDP 是无连接的,每次发送数据都需要指定对端的地址(IP 和端 口)。
  • recvfrom()接收数据,每次都需要传给该方法一个地址结构来存放发送端的地址。 recvfrom()可以接收所有客户端发送给当前应用程序的数据,并不是只能接收某一个客 户端的数据。

tcp和udp特点:

  • tcp:适合下载数据
  • udp:适合视频通话

适用不同场景,互补

  • TCP关闭要挥手
  • UDP没建立连接,不知道关没关。
  • 单个客户端可以一直发,管你收不收我就要发
  • 多个客户端也可以,谁发都行,不是非要和某个进来建立连接,无论谁发数据,服务器都能收

拿发送字节举例:

客户端发送"hello",服务器设置每次只收1字节时,会收到 'h',下次发"abc"收到'a',不会对上次没发完的数据进行保存,因为目的地址不确定是否一样。

代码:

服务器:

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

//服务器
int main()
{
	//创建套接字
	int sockfd=socket(AF_INET,SOCK_DGRAM,0);//tcp:STREAM流协议		
	if(sockfd==-1)
	{
		exit(0);
	}
	 struct sockaddr_in saddr,caddr;//saddr服务器地址,caddr:客户端
	 
	 memset(&saddr,0,sizeof(saddr));//清空
	 saddr.sin_family=AF_INET;
	 saddr.sin_port=htons(6000);//端口,1024以内是知名端口,尽量自己选择较大的端口
	 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//点分十进制转换为32位无符号长整形

	 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	/*if(res==-1)
	{
		printf("bind failed\n");
		exit(0);
	}
   	 res=listen(sockfd,5);
    	if(res==-1)
    	{
        	    exit(0);
    	}*/
	while(1)
	{
		int len=sizeof(caddr);
	/*	int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
       		if(c<0)
		{
			continue;
		}
		printf("accept c=%d\n",c);*/

		char buff[128]={0};
		int n=recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
		printf("ip:%s,buff=%s\n",inet_ntoa(caddr.sin_addr),buff);
		sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
//		close(c);
	}
}

客户端:

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

//客户端
int main()
{
	//创建套接字
	int sockfd=socket(AF_INET,SOCK_DGRAM,0);//tcp:STREAM流协议		
	if(sockfd==-1)
	{
		exit(0);
	}
	 struct sockaddr_in saddr;//服务器地址
	 //清空
	 memset(&saddr,0,sizeof(saddr));
	 saddr.sin_family=AF_INET;
	 saddr.sin_port=htons(6000);//端口
	 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//点分十进制转换为

	while(1)
	{
		printf("input:\n");
		char buff[128]={0};
		fgets(buff,128,stdin);
		if(strncmp(buff,"end",3)==0)
		{
			break;
		}
		sendto(sockfd,buff,strlen(buff)-1,0,(struct sockaddr*)&saddr,sizeof(saddr));
		memset(buff,0,128);
		int len=sizeof(saddr);
		recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
		printf("buff=%s\n",buff);
	}
	close(sockfd);
	exit(0);
}

运行结果:

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值