Linux系统编程——网络编程
Linux 系统编程——网络编程
一、网络编程概述
1.网络通信即Socket编程介绍
(1)网络通信
① 网络通信模式
大部分的网络应用系统可以分为两个部分:客户(Client)和服务器(Server),而网络服务程序架构有两种一种是CS模式,一种是BS模式。
CS即Client/Server(客户机/服务器)结构:C/S结构在技术上很成熟,它的主要特点是交互性强、具有安全的存取模式、网络通信量低、响应速度快、利于处理大量数据。但是该结构的程序是针对性开发,变更不够灵活,维护和管理的难度较大。并且,由于该结构的每台客户机都需要安装相应的客户端程序,分布功能弱且兼容性差,不能实现快速部署安装和配置,因此缺少通用性,具有较大的局限性。QQ就是CS模式,需要安装客户端软件才能访问服务器。
BS即Browser/Server(浏览器/服务器)结构:就是只安装维护一个服务器(Server),而客户端采用**浏览器(Browse)**运行软件。B/S结构应用程序相对于传统的C/S结构应用程序是一个非常大的进步。 B/S结构的主要特点是分布性强、维护方便、开发简单且共享性强、总体拥有成本低。但数据安全性问题、对服务器要求过高、数据传输速度慢、软件的个性化特点明显降低,这些缺点是有目共睹的,难以实现传统模式下的特殊功能要求。例如通过浏览器进行大量的数据输入或进行报表的应答、专用性打印输出都比较困难和不便。此外,实现复杂的应用构造有较大的困难。那像网站、学校教务系统等这些通过浏览器访问的模式叫
BS模式。
② OSI七层网络模型
OSI(Open System Interconnection 开放系统互联)七层网络模型是ISO(International Organization forStandardization,国际标准化组织)提出的一个参考模型,是一个把网络通信在逻辑上的定义,也可以理解成为定义了通用的网络通信规范。而我们的数据在网络中传输的过程,实际上就是如下图的封装和解封装的过程,发送方通过各种封装处理,把数据
转换成比特流的形式,比特流在信号传输的硬件媒介中传输,接收方再把比特流进行解封装处理。
1.物理层
规定了如何为网络通信实现最底层的物理连接,以及物理设备的机械、电气、功能和过程特性。如:如何使用电缆和接头的类型、用来传送信号的电压等。需要注意的是,网络通信过程中所需的物理媒介(网线、线缆等),其实并不属于物理层,因为物理层实际上是一种规定,规定这些物理媒介设备在连接网络时的各种规格、参数以及工作方式。但是同时,双绞线、线缆等物理媒介又是物理层的实现。
2.数据链路层
规定了如何进行物理地址寻址、如何在物理线路上进行数据(帧frame)的可靠传递以及流量控制。数据链路层协议有SLIP协议、CSLIP协议、PPP协议等。交换机,对帧解码并根据帧中包含的信息把数据发送到正确的接收方,所以交换机是工作在数据链路层的。
3.网络层
规定了通过哪些网络节点、什么样的网络路径来将数据(数据包)从发送方发送到接收方。在网络层中,确定了从节点A发数据到节点B的网络路径,经过哪些节点。网络层既可以建立LAN通信系统,更主要的是可以在WAN网络系统中建立通信,这是因为它有自己的路由地址结构,通过路由协议(又称可路由协议)进行网络通信的路由工作。
4.传输层
负责总体的数据传输和数据控制,提供端到端的交换数据的机制。传输层对数据(段)进行分割和重组,并且进行流量控制和根据接收方的接收数据能力确定适当的传输速率。例如以太网无法处理大于1500字节的数据包,传输层将数据分割成数据片段,并对小数据片段进行序列编号。接收方的传输层将根据序列编号对数据进行重组。传输层协议有TCP协议、UDP协议等。
5.会话层
在网络中的两个节点之间建立、维持和终止通信。
6.表示层
在应用程序和网络之间对数据进行格式化,使之能够被另一方理解。即发送方的表示层将应用程序数据的抽象语法转换成网络适用于OSI网络传输的传送语法,接收方则相反。除此之外,表示层还可对数据进行加密与解密。
7.应用层
最顶层的OSI层,为应用程序提供网络服务。如为电子邮件、文件传输功能提供协议支持。应用层协议有HTTP协议、FTP协
议、SMTP协议等。
(2)Socket编程
在UNIX、Linux系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
为了表示和区分已经打开的文件,UNIX/Linux会为每个文件分配一个ID,这个文件就是一个整数,被称为文件描述符
例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
网络连接也是一个文件,它也有文件描述符
我们可以通过 socket()
函数来创建一个网络连接,或者说打开一个网络文件,socket()
的返回值就是文件描述符(注意在windows下的socket返回的叫文件句柄,并不是叫文件描述符)。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
用 read()
读取从远程计算机传来的数据;
用 write()
向远程计算机写入数据。
2.网络编程三要素
(1)协议
协议计算机网络通信必须遵守的规则
上面已经介绍了OSI七层网络模型中,各个层之间都有相对应的协议,这里我们主要关注的是传输层的协议:TCP/UDP协议
①TCP传输协议
TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
TCP建立连接的三次握手::
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
(1)Client首先向Server发送连接请求报文段,同步自己的seq(x),Client进入SYN_SENT状态。
(2)Server收到Client的连接请求报文段,返回给Client自己的seq(y)以及ack(x+1),Server进入SYN_REVD状态。
(3)Client收到Server的返回确认,再次向服务器发送确认报文段ack(y+1),这个报文段已经可以携带数据了。Client进入ESTABLISHED状态。
(4)Server再次收到Client的确认信息后,进入ESTABLISHED状态。
TCP断开连接的四次握手:
(1)Client向Server发送断开连接请求的报文段,seq=m(m为Client最后一次向Server发送报文段的最后一个字节序号加1),Client进入FIN-WAIT-1状态。
(2)Server收到断开报文段后,向Client发送确认报文段,seq=n(n为Server最后一次向Client发送报文段的最后一个字节序号加1),ack=m+1,Server进入CLOSE-WAIT状态。此时这个TCP连接处于半开半闭状态,Server发送数据的话,Client仍然可以接收到。
(3)Server向Client发送断开确认报文段,seq=u(u为半开半闭状态下Server最后一次向Client发送报文段的最后一个字节序号加1),ack=m+1,Server进入LAST-ACK状态。
(4)Client收到Server的断开确认报文段后,向Server发送确认断开报文,seq=m+1,ack=u+1,Client进入TIME-WAIT状态。
(5)Server收到Client的确认断开报文,进入CLOSED状态,断开了TCP连接。
(6)Client在TIME-WAIT状态等待一段时间(时间为2*MSL((Maximum Segment Life)),确认Client向Server发送的最后一次断开确认到达(如果没有到达,Server会重发步骤(3)中的断开确认报文段给Client,告诉Client你的最后一次确认断开没有收到)。如果Client在TIME-WAIT过程中没有再次收到Server的报文段,就进入CLOSES状态。TCP连接至此断开
TCP:服务端和客户端的通信流程:
②UDP传输协议
用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
UDP:服务端和客户端的通信流程:
③TCP/UDP的区别
首先标准套接字分为TCP和UDP协议两种不同type的工作流程,TCP网络编程相对于UDP来说相对复杂,因为TCP是面向连接的服务,其中包括三次握手建立连接的过程,而UDP则是无连接的服务。
(1)TCP面向连接(如打电话要先拨号建立连接),而UDP是无连接的,即发送数据之前不需要建立连接。
(2)TCP提供可靠的服务,也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;而UDP则是尽最大努力进行交付,即不保证可靠交付。
(3)TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;而UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(这样对实时应用很有用,如,IP电话,实时视频会议等)。
(4)每一条的TCP只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信
(5)TCP首部开销20字节;UDP首部开销小,只有8个字节。
(6)TCP的逻辑通信的信道是全双工的可靠信道,而UDP则是不可靠信道。
(2)IP地址
IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。IP地址为32(IPV4)或者128位(IPV6),每一个数据包都必须携带目的地址IP和源IP地址,路由器依靠此信息为数据包选择最优路由(路线)。(IP地址就有点像是几栋楼中的楼的号码)
(3)端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。(注意TCP和UDP的端口号是相互独立的)
实际上通过"IP地址+端口号"来区分不同的服务的
端口提供了一种访问通道,服务器一般都是通过知名的端口来识别的。例如,对于每个TCP/IP的实现来说,FTP服务的端口号是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
端口号,用两个字节表示的整数,它的取值范围是065535。其中0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。(如果IP地址是相当于一栋楼的楼号的话,那么端口号就相当于是这栋楼里面的房间的房号)
利用 协议 + IP地址 + 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
3.字节序
(1)字节系相关概念
字节序:是指多字节数据的存储顺序,在设计计算机系统的时候,有两种处理内存中数据的方法:即大端格式、小端格式。
小端格式(Little-Endian):将低位字节数据存储在低地址。
大端格式(Big-Endian):将高位字节数据存储在低地址。
- 例如: 0x12345678 在不同机器中的存储是不同的,如下所示。
- 为何使用字节序
(1)计算机电路先处理低位字节,效率比较高。因为计算就是从低位开始的,所以计算机内部很多都是小端字节序。
(2)格式规范是为人类编写的,大端字节序更符合人类习惯。
网络字节序的定义:将收到的第一个字节的数据当做高位来看待,这就要求发送端的发送的第一个字节应该是高位。而在发送端发送数据时,发送的第一个字节是该数字在内存中起始地址对应的字节。可见多字节数值在发送前,在内存中数值应该以大端法存放。
所以,网络协议指定了通讯字节序:大端。只有在多字节数据处理时才需要考虑字节序,运行在同一台计算机上的进程相互通信时,一般不用考虑字节序,异构计算机之间进行通讯时,需要将自己的字节序转换为网络字节序。
(2)有关字节序转换的函数
以下所有接口的头文件都是: #include <arpa/inet.h>
①主机字节序转换网络字节序函数
- 函数原型:
uint16_t htons(uint16_t hostshort);//将16位主机字节序数据转换成网络字节序数据
uint32_t htonl(uint32_t hostlong);//将32位主机字节序数据转换成网络字节序数据
- 函数功能:
htons
将16位主机字节序数据转换成网络字节序数据
htonl
将32位主机字节序数据转换成网络字节序数据 - 参数说明:
hostshort
:需要转换的16位主机字节序数据,uint16_t:unsigned short int
hostlong
:需要转换的32位主机字节序数据,uint32_t:32位无符号整型
- 返回值:
成功返回网络字节序的值
②将网络字节序转换主机字节序函数
- 函数原型:
uint16_t ntohs(uint16_t netshort);//将16位网络字节序数据转换成主机字节序数据
uint32_t ntohl(uint32_t netlong);//将32位网络字节序数据转换成主机字节序数据
-
函数功能:
ntohs
将16位网络字节序数据转换成主机字节序数据
ntohl
将32位网络字节序数据转换成主机字节序数据 -
参数说明:
netshort
:需要转换的16位网络字节序数据;uint16_t:unsigned short int
netlong
:需要转换的32位网络字节序数据;uint32_t:unsigned int -
返回值:
成功:返回主机字节序的值
③点分十进制IP地址转换为二进制IP地址函数
- inet_aton函数
- 函数原型
int inet_aton(const char *cp, struct in_addr *inp);
-
函数说明
将点分十进制字符串转换成32位无符号整数,只适用于IPV4地址 -
参数:
cp:将被转换的点分十进制IP地址
inp:保存被转换成二进制的IP地址 -
返回值:
如果地址合法,则返回非0值,反之返回0值 -
inet_pton函数
-
函数原型:
int inet_pton(int af, const char *src, void *dst);
- 函数说明:
该函数将字符串src转换为af地址类型协议簇的网络地址,并存储到dst中。
对于af参数,必须为AF_INET
或AF_INET6
,适用于IPV4和IPV6地址 - 参数说明:
af:AF_INET或AF_INET6
src:将被转换的点分十进制的IP地址
dst:保存被转换的二进制IP地址 - 返回值
转换成功则返回1,对于指定的地址类型协议簇,如果不是一个有效的网络地址,
将转换失败,返回 0,如果指定的地址类型协议簇不合法,将返回-1并,并且errno
设置为EAFNOSUPPORT
④ 二进制IP地址转换成点分十进制的IP地址的函数
- inet_ntoa函数
- 函数原型:
char *inet_ntoa(struct in_addr in);
-
函数说明:
inet_ntoa()
用来将参数in所指的大端网络字节序二进制的数字转换成ipv4点分十进制字符串网络地址,然后将指向此网络地址字符串的指针返回。成功则返回字符串指针,失败则返回NULL。 -
参数:
in:将被转换的二进制IP地址(即32位无符号整数) -
返回值:
成功:返回点分十进制的IP地址
失败:返回NULL -
inet_ntop函数
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 函数说明:
将二进制的IP地址转换成点分十进制的字符串(即点分十进制的IP地址)
该函数将地址类型协议簇为af的网络地址src转换为字符串,并将其存储到dst中,其中dst不能是空指针。
调用者在参数size中指定可使用的缓冲字节数。
inet_ntop
拓展自inet_ntoa
来支持多种地址类型协议簇,inet_ntoa
现在已经被弃用。 - 参数:
af:AF_INET或AF_INET6
src:二进制的IP地址
dst:保存被转换成点分十进制的IP地址
size:指定可使用的缓冲字节数 - 返回值:
inet_ntop
执行成功,返回一个指向dst
的非空指针,如果执行失败,将返回NULL,并且errno
设置为相应的错误类型。
二、socket套接字相关函数
1.socket 编程流程
(1)TCP通信流程
-
服务器
1.创建套接字(socket)
2.将socket与IP地址和端口绑定(bind)
3.监听被绑定的端口(listen)
4.接收连接请求(accept)
5.从socket中读取客户端发送来的信息(read)
6.向socket中写入信息(write)
7.关闭socket(close) -
客户端
1.创建套接字(socket)
2.连接指定计算机的端口(connect)
3.向socket中写入信息(write)
4.从socket中读取服务端发送过来的消息(read)
5.关闭socket(close)
(2)UDP通信的过程
-
服务器:
1.使用函数socket(),生成套接字文件描述符;
2.通过struct sockaddr_in 结构设置服务器地址和监听端口;
3.使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;
4.接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
5.向客户端发送数据,使用sendto() 函数向服务器主机发送数据;
6.关闭套接字,使用close() 函数释放资源;
客户端:
1.使用socket(),生成套接字文件描述符;
2.通过struct sockaddr_in 结构设置服务器地址和监听端口;
3.向服务器发送数据,sendto() ;
4.接收服务器的数据,recvfrom() ;
5.关闭套接字,close() ;
2.socket 相关函数
(1)socket()函数(创建套建字)
- 函数原型:
#include <sys/types.h>
#include <sys/socket.h>//需要引入的头文件
int socket(int domain, int type, int protocol);
-
函数说明:
用于创建套接字,同时指定协议和类型 -
参数:
domain:指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);AF_INET IPv4因特网域 AF_INET6 IPv6因特网域 AF_UNIX Unix域 AF_ROUTE 路由套接字 AF_KEY 密钥套接字 AF_UNSPEC 未指定
type:参数指定socket
的类型:
SOCK_STREAM
:
流式套接字提供可靠的,面向连接的通信流,它使用TCP协议,从而保证了数据传输的正确性和顺序性
SOCK_DGRAM
:
数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠,无差错的。它使用数据报协议UDP
SOCK_RAW
:
允许程序使用底层协议,原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。
protocol:通常赋值为“0”
0选择type类型对应的默认协议
IPPROTO_TCP
TCP传输协议
IPPROTO_UDP
UDP传输协议
IPPROTO_SCTP
SCTP传输协议
IPPROTO_TIPC
TIPC传输协议
- 返回值:成功返回非负套接字描述符,失败返回-1
(2)bind()函数(绑定套建字)
- 函数原型:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
-
函数说明: 用于绑定IP地址和端口号到socket
-
参数:
sockfd:是一个socket描述符
addr:是一个指向包含有本机IP地址及端口号等信息的sockaddr
类型的指针,指向要绑定给sockfd
的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同。说明: sockaddr在头文件`#include <sys/socket.h>`中定义, sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,
该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* 地址族Address family */
__be16 sin_port; /* 16位端口号Port number */
struct in_addr sin_addr; /* 网络地址Internet address */
};
/* Internet address. */
struct in_addr {
__be32 s_addr;//32位网络地址
};
上述两者结构体长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in
结构的指针也可以指向sockaddr
。
一般先把sockaddr_in
变量赋值后,强制类型转换后传入用sockaddr
做参数的函数:sockaddr_in
用于socket定义和赋值;sockaddr
用于函数参数。
addrlen:地址的长度,一般用sizeof(struct sockaddr_in)
表示
- 返回值:成功返回0,失败返回-1
(3)listen()函数(监听被绑定的端口)
- 函数原型:
int listen(int sockfd, int backlog);
- 函数说明:
功能:
设置能处理的最大连接数,listen并未开始接受连线,只是设置了socket的listen模式,listen函数只用于服务器端,服务器进程不知道要与谁进行连接,因此,它不会主动的要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接,主要就连个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
内核为任何一个给定监听套接字维护两个队列:
未完成连接队列,每个这样的SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程,这些套接字处于SYN_REVD
状态
已完成连接队列,每个已完成TCP三次握手过程的客户端对应其中一项,这些套接字处于ESTABLISHED
状态; - 参数:
sockfd:socket系统调用返回的服务端socket描述符
backlog:指定在请求队列中允许的最大的请求数,大多数系统默认为5
返回值:成功返回0,失败返回-1
(4)accept()函数(接收连接请求)
- 函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
函数说明:
accept函数由TCP服务器调用,用于从已完成连接队列对头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠。 -
参数:
sockfd:是socket系统调用返回的服务器端socket描述符
addr:用来返回已连接的对端(客户端)的协议地址
addrlen:客户端地址长度,注意需要取地址 -
返回值:
该函数的返回值是一个新的套接字的描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符,一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
(5)connect()函数(发送连接请求)
- 函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
函数说明:
该函数用于绑定之后的client端(客户端),与服务器建立连接 -
参数:
sockfd:创建的socket描述符
addr:服务端的ip地址和端口号的地址结构指针
addrlen:地址的长度,通常被设置为sizeof(struct sockaddr) -
返回值:成功返回0,遇到错误时返回-1,并且errno中包含相应的错误码
(6)send()、recv()(TCP发送、接收信息)
- send函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
函数说明:
函数只能对处于连接状态的套接字进行使用,参数sockfd为已建立好连接的套接字描述符 -
参数:
sockfd:为已建立好连接的套接字描述符即accept
函数的返回值
buf:要发送的内容
len:发送内容的长度
flags:设置为MSG_DONTWAITMSG
时 表示非阻塞,设置为0时 功能和write一样 -
返回值:成功返回实际发送的字节数,失败:返回 -1
-
recv函数原型:
ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
-
函数说明:接收套接字中的数据
-
参数:
sockfd:在哪个套接字接
buf:存放要接收的数据的首地址
len:要接收的数据的字节
flags:设置为MSG_DONTWAITMSG
时 表示非阻塞,设置为0时 功能和read一样 -
返回值:成功返回实际发送的字节数,失败:返回 -1
(7)sendto()、recvfrom()函数(UDP发送、接收消息)
- sendto()函数原型:
int sendto(int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);
-
函数说明:UDP发送消息
-
参数说明:
s: socket描述符;
buf:UDP数据报缓存区(包含待发送数据);
len: UDP数据报的长度;
flags:调用方式标志位(一般设置为0),设置为MSG_DONTWAITMSG
时 表示非阻塞
to: 指向接收数据的主机地址信息的结构体(sockaddr_in需类型转换);之前介绍bind函数时已经介绍。
tolen:to所指结构体的长度; -
返回值:成功则返回实际传送出去的字符数,失败返回-1,错误原因会存于errno 中。
-
recvfrom()函数原型:
int recvfrom(int s, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen);
- 函数说明:UDP接收消息
- 参数:
s: socket描述符;
buf: UDP数据报缓存区(包含所接收的数据);
len: 缓冲区长度。
flags: 调用操作方式(一般设置为0),设置为MSG_DONTWAITMSG
时 表示非阻塞
from: 指向发送数据的客户端地址信息的结构体(sockaddr_in需类型转换);
fromlen:指针,指向from结构体长度值。 - 返回值:成功则返回实际接收到的字符数,失败返回-1,错误原因会存于errno 中。
3.示例代码
- 实现客户端,服务器通信
- 服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
int main()
{
int s_fd;//socket 返回的套接字,服务器端
int c_fd;//accept函数返回的客户端的套接字
int ret;
int c_len;//客户端结构体的大小
int readSize;
int quit_flag = 0;
pid_t pid;
char readBuf[128] = {'\0'};//存放读取的客户端内容
char writeBuf[128] = {'\0'};//存放发往客户端的内容
char ipBuf[32] = {'\0'};
struct sockaddr_in s_addr;//设置本机ip地址及端口的结构体
struct sockaddr_in c_addr;//接收的客户端的ip地址及端口的结构体
//1.创建套接字(socket)
s_fd = socket(AF_INET,SOCK_STREAM, 0);
if(s_fd<0){
perror("creat socket fail");
return -1;
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8989);//绑定端口号,并将端口号变为网络字节序
ret = inet_aton("192.168.109.137",&s_addr.sin_addr);//绑定本机IP地址,并将其转换为二进制IP地址
if(ret == 0){
perror("inet_aton fail");
return -1;
}
//2.将socket与IP地址和端口绑定(bind)
ret = bind(s_fd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr_in));//注意将struct sockaddr_in*强制转换为struct sockaddr*
if(ret < 0){
perror("bind fail");
return -1;
}
//3.监听被绑定的端口(listen)
listen(s_fd,5);
c_len = sizeof(struct sockaddr_in);
while(1){
//4.接收连接请求(accept)
c_fd = accept(s_fd, (struct sockaddr*)&c_addr, &c_len);
if(c_fd < 0){
perror("accept error");
}else{
memset(ipBuf,'\0',32);
strcpy(ipBuf,inet_ntoa(c_addr.sin_addr));
printf("get connect:%s\n",ipBuf);//将客户度链接的地址转换为十进制打印
pid = fork();
//创建子进程对接每个客户端
if(pid == 0)
{
pid_t pid;
pid = fork();
if(pid > 0){
while(1){
printf("message to the IP :%s:",ipBuf);
memset(writeBuf,'\0',128);
scanf("%s",writeBuf);
write(c_fd,writeBuf,strlen(writeBuf));
}
}else if(pid == 0){
while(1){
//从socket中读取客户端发送来的信息(read)进程
memset(readBuf,'\0',sizeof(readBuf));
readSize = read(c_fd,readBuf,128);
if(readSize < 0){
perror("read fail");
}else if(readSize == 0){
printf("client quits\n");
break;
}else{
printf("IP %s :%s\n",inet_ntoa(c_addr.sin_addr),readBuf);
}
}
//发送信息到客户端
}
}
}
}
//7.关闭socket(close)
close(c_fd);
close(s_fd);
return 0;
}
- 客户端
#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int c_fd;//客户端套接字
int ret;
int readSize;
pid_t pid;
char readBuf[128] = {'\0'};//存放读取的服务器内容
char writeBuf[128] = {'\0'};//存放发往服务器的内容
struct sockaddr_in addr;//想要连接的目标地址
if(argc < 3){
printf("the param is not good! Please in put ip and port\n");
exit(-1);
}
//1.创建套接字(socket)
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd<0){
perror("creat socket fail");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));//设置目标端口
inet_aton(argv[1],&addr.sin_addr);//设置目标IP地址
//2.连接指定计算机的端口(connect)
ret = connect(c_fd,(struct sockaddr*)&addr,sizeof( struct sockaddr_in));
if(ret<0 ){
perror("connect error");
return -1;
}
pid = fork();
if(pid > 0){
while(1)
{
//3.从socket中读取服务端发送过来的消息(read)
memset(readBuf,0,sizeof(readBuf));
readSize = read(c_fd,readBuf,128);
if(readSize == -1)
{
perror("read");
}
printf("get %d from the sever:%s\n",readSize,readBuf);
}
}else if(pid ==0){
while(1){
//4.向服务区中发送数据
printf("Please input:");
memset(writeBuf,'\0',128);
scanf("%s",writeBuf);
write(c_fd,writeBuf,strlen(writeBuf));
if(strstr(writeBuf,"quit")!=NULL){
kill(getppid(),9);
kill(getpid(),9);
}
}
}
return 0;
}
- 运行结果
服务器
客户端:
服务器:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)