linux网络编程系列(一)--OSI七层模型和TCP-IP四层模型
1. ISO七层网络模型
为使不同计算机厂家之间的计算机能够互相通信,以便在更大的范围内建立计算机网络,国际标准化组织ISO在1981年正式推出了一个网络系统结构--OSI七层网络模型,此后各大计算机厂商迅速向它靠拢,大大推动了计算机网络的发展。 OSI七层网络模型分别是:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
1.1 应用层
应用层是最靠近用户的OSI层,这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务,主要负责对软件提供接口以使程序能使用网络服务,从实现上讲,其实就是使用了特定的端口号和特定的数据格式封装了tcp或者udp,从而产生了应用层协议。
1.2 表示层
表示层是应用程序和网络之间的翻译官,可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。在表示层,数据将按照网络能理解的方案进行格式化,这种格式化也因所使用网络的类型不同而不同。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符,如有必要,表示层会通过使用一种通用格式来实现多种数据格式之间的转换。 一些典型的应用如下: + 数据的解密与加密,如系统口令的处理; + 对图片和文件格式信息进行解码和编码;
1.3 会话层
会话层负责在网络中的两节点之间建立、维持和终止通信。会话层的功能包括:建立通信连接,保持会话过程通信连接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时从何处开始重新发送。你可能常常听到有人把会话层称作网络通信的“交通警察”。当通过拨号向你的 ISP (因特网服务提供商)请求连接到因特网时,ISP 服务器上的会话层向你与你的PC客户机上的会话层进行协商连接。若你的电话线偶然从墙上插孔脱落时,你终端机上的会话层将检测到连接中断并重新发起连接。
1.4 传输层
传输层是OSI七层模型中最重要的一层,传输协议同时进行流量控制或是基于接收方可接收数据的快慢程度规定适当的发送速率。除此之外,传输层按照网络能处理的最大尺寸将较长的数据包进行强制分割。例如,以太网无法接收大于1 5 0 0 字节的数据包,发送方节点的传输层将数据分割成较小的数据片,同时对每一数据片安排一个序列号,以便数据到达接收方节点的传输层时,能以正确的顺序重组。该过程即被称为排序。工作在传输层的一种服务是 T C P / I P 协议套中的T C P (传输控制协议),另一项传输层服务是I P X / S P X 协议集的S P X (序列包交换)。
1.5 网络层
网络层主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方。网络层通过综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定从一个网络中节点A 到另一个网络中节点B的最佳路径。由于网络层处理,并智能指导数据传送,路由器连接网络各段,所以路由器属于网络层。在网络中,“路由”是基于编址方案、使用模式以及可达性来指引数据的发送。 网络层负责在源机器和目标机器之间建立它们所使用的路由,这一层本身没有任何错误检测和修正机制,因此,网络层必须依赖于端端之间可靠传输服务。 网络层用于本地LAN网段之上的计算机系统建立通信,它之所以可以这样做,是因为它有自己的路由地址结构,这种结构与第二层机器地址是分开的、独立的。这种协议称为路由或可路由协议。路由协议包括I P、N o v e l l公司的I P X以及A p p l e Ta l k协议。 网络层是可选的,它只用于当两个计算机系统处于不同的由路由器分割开的网段这种情况,或者当通信应用要求某种网络层或传输层提供的服务、特性或者能力时。例如,当两台主机处于同一个L A N网段的直接相连这种情况,它们之间的通信只使用L A N的通信机制就可以了(即OSI 参考模型的一二层)。
1.6 数据链路层
数据链路层控制网络层与物理层之间的通信。它的主要功能是如何在不可靠的物理线路上进行数据的可靠传递。为了保证传输,从网络层接收到的数据被分割成特定的可被物理层传输的帧。帧是用来移动数据的结构包,它不仅包括原始数据,还包括发送方和接收方的物理地址以及检错和控制信息。其中的地址确定了帧将发送到何处,而纠错和控制信息则确保帧无差错到达。 如果在传送数据时,接收点检测到所传数据中有差错,就要通知发送方重发这一帧。 数据链路层的功能独立于网络和它的节点和所采用的物理层类型,它也不关心是否正在运行 Wo r d 、E x c e l 或使用I n t e r n e t 。有一些连接设备,如交换机,由于它们要对帧解码并使用帧信息将数据发送到正确的接收方,所以它们是工作在数据链路层的。
数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。 数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。
1.7 物理层
物理层是最底层,该层包括物理连网媒介,如电缆连线连接器。物理层的协议产生并检测电压以便发送和接收携带数据的信号。在你的桌面P C上插入网络接口卡,你就建立了计算机连网的基础。换言之,你提供了一个物理层。尽管物理层不提供纠错服务,但它能够设定数据传输速率并监测数据出错率。网络物理问题,如电线断开,将影响物理层。 用户要传递信息就要利用一些物理媒体,如双绞线、同轴电缆等,但具体的物理媒体并不在OSI的7层之内,有人把物理媒体当做第0层,物理层的任务就是为它的上一层提供一个物理连接,以及它们的机械、电气、功能和过程特性。如规定使用电缆和接头的类型、传送信号的电压等。在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,单位是bit比特。 主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
2. TCP/IP四层模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层(对应OSI的第四层)。
2.1 四层模型
TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这4层分别为: + 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。 负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。
- 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。 负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。
- 网络层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。 负责将数据帧封装成IP数据报,并运行必要的路由算法。
- 数据链路层:对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据,实现网卡接口的网络驱动程序。负责将二进制流转换为数据帧,并进行数据帧的发送和接收。
2.2 四层模型分别有哪些协议
- 应用层{http超文本传输协议 ftp文件传输协议 telnet远程登录 ssh安全外壳协议 stmp简单邮件发送 pop3收邮件}
- 传输层{tcp传输控制协议,udp用户数据包协议}
- 网络层{ip网际互联协议 icmp网络控制消息协议 igmp网络组管理协议}
- 数据链路层{arp地址转换协议,rarp反向地址转换协议,mpls多协议标签交换}
部分协议注解如下: ARP:(地址转换协议)用于获得同一物理网络中的硬件主机地址。 MPLS:(多协议标签交换)很有发展前景的下一代网络协议。 IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。 ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。 IGMP:(网络组管理协议)被IP主机用来向本地多路广播路由器报告主机组成员的协议。 TCP:(传输控制协议)为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到相应的应用程序。 UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。
2.3 最常见的应用层协议
注意:可以使用cat /etc/services来查看有哪些应用层协议,以及他们使用了传输层的哪些协议。 通常我们是使用ip地址后面跟上端口号来唯一确定一个连接:ip用来定位主机,port区别应用(进程)。 例如: http-->80 ssh-->22 telnet-->23 ftp-->21
系统使用端口号一般是1-1024,用户可使用的是1025-65536。
linux网络编程系列(二)-1socket套接字基本概念详解
1. 网络编程基本概念
1.1 什么是套接字
套接字,也叫socket,是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户。网络通信,说白了就是进程间的通信(同一台机器上不同进程或者不同计算机上的进程间通信)。
在网络中,每一台计算机或者路由都有一个网络地址,就是IP地址。两个进程通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上一般都是同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,比如端口号和协议。
1.2 端口号的概念
在网络的世界里,端口大致有两种: + 一是物理意义上的端口,如交换机、路由器等用于连接其他网络设备的接口; + 二是指TCP/IP协议族中的端口号;
端口号的范围从0-65535,分类如下: + 一类是众所周知的,公用的端口号,其值一般为0~1024,例如http的端口号是80,ftp为21,ssh为22,telnet为23等; + 一类是用户自己定义的,通常是大于1024并且小于65535的整型值;
1.3 ip地址的表示
通常我们在表达IP地址时习惯使用点分十进制表示的数值(或者是为冒号分开的十六进制Ipv6地址),而在socket编程中使用的则是二进制值,这就需要对这两个数值进行转换。
ipv4地址:32bit, 4字节,相当于一个整型,通常采用点分十进制记法,例如对于:10000000 00001011 00000111 00011111, 点分十进制表示为:128.11.7.31。
2. socket的概念
socket是一种特殊的I/O接口,它也是一种文件描述符。如第一节所说,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
- 每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;
- 一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示;
socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过这个socket描述符来实现的。
2.1 socket类型
2.1.1 流式socket(SOCK_STREAM)
用于TCP通信,流式套接字提供可靠的、面向连接的通信流,使用TCP协议,从而保证了数据传输的正确性和顺序性。
2.1.2 数据报socket(SOCK_DGRAM)
用于UDP通信,数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且是不可靠的,它使用数据报协议UDP。
2.1.3 原始socket (SOCK_RAW)
用于新的网络协议实现的测试等,原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些自定义协议的开发。
2.2 socket信息数据结构
//头文件<netinet/in.h> sockaddr和sockaddr_in大小一致
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/
};
struct sockaddr_in
{
short int sa_family; /*地址族 AF_INET IPv4协议 AF_INET6 IPv6协议*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};
struct in_addr
{
unsigned long int s_addr; /* 32位IPv4地址,网络字节序 */
};
2.3 数据存储字节序的转换
计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式)。
- 内存的低地址存储数据的低字节,高地址存储数据的高字节的方式叫小端模式;
- 内存的高地址存储数据的低字节,低地址存储数据高字节的方式称为大端模式;
eg,对于内存中存放的数0x12345678来说: + 如果是采用大端模式存放的,则其真实的数是:0x12345678; + 如果是采用小端模式存放的,则其真实的数是:0x78563412;
端口号和IP地址都是以网络字节序存储的,不是主机字节序,网络字节序都是大端模式,而主机字节序则一般都是小端模式(也有特殊的是大端模式,这里不考虑)。所以在网络连接过程中,要把主机字节序和网络字节序相互对应起来,需要对这两个字节存储顺序进行转换。
这里用到四个函数:htons(),ntohs(),htonl()和ntohl().这四个函数分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //将主机的无符号长整型数转换成网络字节序
uint16_t htons(uint16_t hostshort); //将主机的无符号短整形数转换成网络字节序
uint32_t ntohl(uint32_t netlong); //将一个无符号长整型数从网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort); //将一个无符号短整形数从网络字节序转换为主机字节序
2.4 IP地址格式转化
通常在表达地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在socket编程中使用的则是32位的网络字节序的二进制值,这就需要对这两个数值进行转换。
这里在Ipv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPV4和Ipv6兼容的函数有inet_pton()和inet_ntop()。
2.4.1 IPv4的函数原型
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *straddr, struct in_addr *addrptr);
char *inet_ntoa(struct in_addr inaddr);
in_addr_t inet_addr(const char *straddr);
函数inet_aton():将点分十进制数的IP地址转换成为网络字节序的32位二进制数值。返回值:成功,则返回1,不成功返回0. + 参数straddr:存放输入的点分十进制数IP地址字符串。 + 参数addrptr:传出参数,保存网络字节序的32位二进制数值。
函数inet_ntoa():将网络字节序的32位二进制数值转换为点分十进制的IP地址。
函数inet_addr():功能与inet_aton相同,但是结果传递的方式不同。inet_addr()若成功则返回32位二进制的网络字节序地址。
2.4.2 IPv4和IPv6兼容的函数原型
#include <arpa/inet.h>
int inet_pton(int family, const char *src, void *dst);
const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);
函数inet_pton跟inet_aton实现的功能类似,只是多了family参数,该参数指定为AF_INET,表示是IPv4协议,如果是AF_INET6,表示IPv6协议。
函数inet_ntop跟inet_ntoa类似,其中len表示表示转换之后的长度(字符串的长度)。
2.4.3 具体实现代码
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
char ip[] = "192.168.0.101";
struct in_addr myaddr;
memset((void*)&myaddr, 0, sizeof(struct in_addr));
/* inet_aton */
int iRet = inet_aton(ip, &myaddr);
if ( iRet == 1)
{
printf("%ld\n", myaddr.s_addr);
/* inet_addr */
printf("%x\n", inet_addr(ip));
}
else
{
printf("call inet_aton failed\n");
}
/* inet_pton */
iRet = inet_pton(AF_INET, ip, &myaddr);
if ( iRet == 1 )
{
printf("%x\n", myaddr.s_addr);
}
else
{
printf("call inet pton failed\n");
}
myaddr.s_addr = 0xac100ac4;
/* inet_ntoa */
printf("%s\n", inet_ntoa(myaddr));
/* inet_ntop */
inet_ntop(AF_INET, &myaddr, ip, 16);
puts(ip);
return 0;
}
3. 域名与IP地址的对应关系
一般来讲,我们在上网的过程中都不愿意记忆冗长的IP地址,尤其到Ipv6时,地址长度多达128位,那时就更加不可能一次性记忆那么长的IP地址了。我们一般记住的,都是这个网站的域名地址。
大家都知道,百度的域名为:http://www.baidu.com,而这个域名其实对应了一个百度公司的IP地址,那么百度公司的IP地址是多少呢?
我们可以利用ping http://www.baidu.com来得到百度公司的ip地址。那么,系统是如何将http://www.baidu.com 这个域名转化为IP地址的呢?
在linux中,最常用的是gethostbyname()和gethostbyaddr(),它们都可以实现IPv4/IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()则是逆操作,是将IP地址转化为主机名。
函数原型:
#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);
结构体:
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*主机IP地址类型 IPv4为AF_INET*/
int h_length; /*主机IP地址字节长度,对于IPv4是4字节,即32位*/
char **h_addr_list; /*主机的IP地址列表*/
}
#define h_addr h_addr_list[0] /*保存的是ip地址*/
- gethostbyname():用于将域名(http://www.baidu.com)或主机名转换为IP地址。参数hostname指向存放域名或主机名的字符串。
- gethostbyaddr():用于将IP地址转换为域名或主机名。参数addr是一个IP地址,此时这个ip地址不是普通的字符串,而是要通过函数inet_aton()转换。len为IP地址的长度,AF_INET为4。family可用AF_INET:Ipv4或AF_INET6:Ipv6。
Example:
//test.cpp 将百度的www.baidu.com 转换为ip地址
#include <netdb.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char **argv)
{
char *ptr, **pptr;
struct hostent *hptr = NULL;
char str[32] = {0};
if ( argc < 2 )
{
printf("please input an addr,eg:./a.out www.baidu.com\n");
return 0;
}
/* 取得命令后第一个参数,即要解析的域名或主机名 */
ptr = argv[1];
/* 调用gethostbyname()。结果存在hptr结构中 */
if((hptr = gethostbyname(ptr)) == NULL)
{
printf("gethostbyname error for host:%s\n", ptr);
return 0;
}
else
{
/* 将主机的规范名打出来 */
printf("official hostname:%s\n", hptr->h_name);
}
/* 主机可能有多个别名,将所有别名分别打出来 */
for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)
printf("alias:%s\n", *pptr);
/* 根据地址类型,将地址打出来 */
switch(hptr->h_addrtype)
{
case AF_INET:
case AF_INET6:
pptr = hptr->h_addr_list;
/* 将刚才得到的所有地址都打出来。其中调用了inet_ntop()函数 */
for(; *pptr!=NULL; pptr++ )
{
printf("address:%s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
}
printf("first address: %s\n", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
break;
default:
printf("unknown address type\n");
break;
}
return 0;
}
编译运行 g++ test.cpp ./a.out http://www.baidu.com
official hostname:www.a.shifen.com
alias:www.baidu.com
address:14.215.177.39
address:14.215.177.38
first address: 14.215.177.39
linux网络编程系列(三)--tcp和udp的基本函数调用过程及如何选择
1. socket编程
1.1 概述
TCP是TCP/IP体系中面向连接的传输层协议,它提供全双工和可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。
首先,TCP要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。
其次,TCP采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。
最后,采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP采用可变长的滑动窗口,使得发送端与接收端可根据自己的CPU和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。
1.2 tcp服务端编程
1.2.1 TCP通信的基本步骤
服务端:socket---bind---listen---while(1){---accept---recv---send---close---}---close 客户端:socket----------------------------------connect---send---recv-----------------close
1.2.2 服务器端头文件包含
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
1.2.3 socket函数
功能:生成一个套接口描述符 原型:int socket(int domain,int type,int protocol); 参数: domain{ AF_INET:Ipv4网络协议 AF_INET6:IPv6网络协议} type{tcp:SOCK_STREAM udp:SOCK_DGRAM} protocol 指定socket所使用的传输协议编号,常用的协议有:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,他们分别对应TCP协议、UDP协议、STCP协议、TIPC协议,当protocol默认为0时,则根据type参数的值,自动选择协议类型; 返回值:成功则返回套接口描述符,失败返回-1。 常用实例:
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
1.2.4 bind函数
功能:用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。 原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen); 参数: sockfd为前面socket的返回值 addrlen sockaddr的结构体长度。通常是计算sizeof(struct sockaddr); my_addr为结构体指针变量 对于不同的socket domain定义了一个通用的数据结构:
//此结构体不常用
struct sockaddr
{
unsigned short int sa_family; //调用socket()时的domain参数,即AF_INET值。
char sa_data[14]; //最多使用14个字符长度
};
//此sockaddr结构会因使用不同的socket domain而有不同结构定义, 例如使用AF_INET domain,其socketaddr结构定义便为:
struct sockaddr_in //常用的结构体
{
unsigned short int sin_family; //即为sa_family AF_INET
uint16_t sin_port; //为使用的port编号
struct in_addr sin_addr; //为IP 地址
unsigned char sin_zero[8]; //未使用
};
struct in_addr
{
uint32_t s_addr;
};
返回值:成功则返回0,失败返回-1,并设置errno,最常见的errno有以下两种: + EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问,比如如果绑定在1-1023端口的时候,就会报该错误。 + EADDRINUSE,被绑定的地址正在使用中,比如将socket绑定在一个处于TIME_WAIT状态的socket地址。
常用实例:
struct sockaddr_in my_addr; //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
//或bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用Ipv4网络协议
my_addr.sin_port = htons(8888); //表示端口号为8888,通常是大于1024的一个值。
//htons()用来将参数指定的16位hostshort转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); // //inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为INADDR_ANY,这表示服务器自动填充本机IP地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)
{perror("bind");close(sfd);exit(-1);}
/*通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址*/
注意:如果my_addr.sin_addr.s_addr = htonl(INADDR_ANY)时,INADDR_ANY就是指定地址为0.0.0.0的地址,它其实是表示不确定地址,一般是用于多网卡的机器上,也就是有多个IP地址,如果设置了INADDR_ANY,那么根据端口号,无论连接哪个IP地址,都是可以连接成功的。
1.2.5 listen函数
功能:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。 原型:int listen(int sockfd, int backlog); 参数: sockfd为前面socket的返回值.即sfd backlog指定同时能处理的最大连接要求,通常为10或者5。 最大值可设至128 返回值:成功则返回0,失败返回-1 常用实例:
if(listen(sfd, 10) == -1)
{
perror("listen");
close(sfd);
exit(-1);
}
1.2.6 accept函数
功能:接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求。 原型:int accept(int s,struct sockaddr * addr,int * addrlen); 参数: s为前面socket的返回值,即sfd addr为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。 addrlen表示结构体的长度,为整型指针 返回值:成功则返回新的文件描述符new_fd,失败返回-1 常用实例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
注意:accept函数只是从监听队列中取出连接,而不论连接处于什么状态,网络状况有什么变化。
1.2.7 recv函数
功能:接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间 原型:int recv(int sockfd,void *buf,int len,unsigned int flags); 参数:sockfd为前面accept的返回值.即new_fd,也就是新的套接字。 buf表示缓冲区 len表示缓冲区的长度 flags通常为0 返回值: >0 成功,返回实际接收到的字符数; =0 连接关闭,说明recv在等待接收数据时网络中断了; -1 表示出错; 常用实例:
char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{
perror("recv");
close(new_fd);
close(sfd);
exit(-1);
}
puts(buf);
注意:read函数返回值不一样,大于0 是返回字节数,等于0是读到文件末尾了,小于0则表示出现了错误,如果错误为EINTR说明是由中断引起的,如果是ECONNNERST则表示网络连接出现了问题。
1.2.8 send函数
功能:发送数据给指定的远端主机 原型:int send(int s,const void * msg,int len,unsigned int flags); 参数:s为前面accept的返回值.即new_fd msg一般为常量字符串 len表示长度 flags通常为0 返回值: >0 成功,返回实际传送出去的字符数,可能会少于你所指定的发送长度; =0 连接关闭,网络中断了; -1 表示出错; 常用实例:
if(send(new_fd, "hello", 6, 0) == -1)
{
perror("send");
close(new_fd);
close(sfd);
exit(-1);
}
注意:write函数返回小于0时,如果为EPIPE,则表示网络连接出现了问题。
1.2.9 close函数
功能:当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源 原型:int close(int fd); 参数:fd为前面的sfd,new_fd 返回值:若文件顺利关闭则返回0,发生错误时返回-1 常用实例:close(new_fd); close(sfd); 注意:在多进程并发服务器中,父子进程共享套接字,有多少个进程共享该套接字,该套接字就有多少个引用计数,此时其中一个进程调用close只是关闭了当前进程的这个文件描述符,但并没有发生四次挥手,直到这个套接字的引用计数为0时,才会发生四次挥手
1.2.10 sockatmark函数
功能:判断sockfd是否处于带外标记,即判断下一个读取的数据是否含有带外数据,若含有,则调用带MSG_OOB标志的recv来读取带外数据 原型:int sockatmark(int sockfd); 参数:fd为前面的sfd,new_fd 返回值:返回1则说明下一个数据时带外数据,若返回0,则不是
1.3 tcp客户端编程
1.3.1 connect函数
功能:用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去。 原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen); 参数:sockfdà为前面socket的返回值,即sfd serv_addrà为结构体指针变量,存储着远程服务器的IP与端口号信息。 addrlenà表示结构体变量的长度 返回值:成功则返回0,失败返回-1 常用实例:
struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服务器的ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{perror("connect");close(sfd);exit(-1);}
将上面的头文件以及各个函数中的代码全部拷贝就可以形成一个完整的例子,此处省略。 还可以不写客户端程序,使用telnet远程登录来检测我们的服务器端程序。比如我们的服务器程序在监听8888端口,我们可以用telnet 192.168.0.101 8888来查看服务端的状况。
2. tcp编程实现
2.1 使用类封装tcp的基本操作
//头文件 SxTcp.h
#ifndef __SXTCP_H__
#define __SXTCP_H__
#include <stdio.h>
#define TIMEOUT_SEC 1
#define MAX_READ_SIZE BUFSIZ
#define DEFAULT_EPOLL_FD_NUM 1024
//Tcp类
class CTcp
{
//构造函数
public:
CTcp ();
CTcp (int nSock);
virtual ~CTcp ();
//重载操作符
public:
int operator = (int);//赋值
int operator != (int) const;//不等于操作符
int operator == (int) const;//等于操作符
//公有成员函数
public:
int GetHandle () const;//取出m_nSock
int Open ();//创建socket
int Close ();//关闭监听socket
int Connect (const char *, int) const;//连接(未设置超时)
int Bind (const char *, int) const;//绑定
int Listen (int nNum) const;//监听
int Accept () const;//接受连接
int Recv(int nFd, char* buf, int nBufLen);//服务端接收
int Send(int nFd, char* buf, int nBufLen);//服务端发送
void Close (int nFd);//服务端关闭连接socket
int Send (const void *, int, int = TCP_TIMEOUT) const;//客户端发送数据
int Recv (void *, int, int = TCP_TIMEOUT) const;//客户端接收数据
static const int SOCK_ERROR;
static const int SOCK_TIMEOUT;
static const long TCP_TIMEOUT;
//私有成员变量
private:
int m_nSock;
};
#endif
//sxTcp.cpp
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <stdarg.h>
#include <arpa/inet.h>
#include <iostream>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <netdb.h>
#include <errno.h>
#include <assert.h>
#include <sys/epoll.h>
#include <signal.h>
#include "SxTcp.h"
const int CTcp::SOCK_ERROR = -100;
const int CTcp::SOCK_TIMEOUT = -101;
const long CTcp::TCP_TIMEOUT = 500000;
//构造函数
CTcp::CTcp ()
{
m_nSock = -1;
}
//构造函数
CTcp::CTcp (int p_iSock)
{
m_nSock = p_iSock;
}
//析构函数
CTcp::~CTcp ()
{
Close ();
}
/*赋值
入参:nSockfd - socket句柄
出参:赋值后的socket句柄
*/
int CTcp::operator = (int p_iSockfd)
{
//isfdtype判断nSockfd是否为指定类型,S_IFSOCK判断是否为套接字描述符,返回1是,0不是,-1出错
assert ((m_nSock == -1) && (p_iSockfd > -1) && (isfdtype (p_iSockfd, S_IFSOCK) == 1));
m_nSock = p_iSockfd;
return m_nSock;
}
/*判断两个socket句柄是否不相等
入参:n - "!="右边的socket句柄
出参:1:不相等;0:相等
*/
int CTcp::operator != (int p_iSockfd) const
{
return (m_nSock != p_iSockfd);
}
/*判断两个socket句柄是否相等
入参:n - "=="右边的socket句柄
出参:1:相等;0:不相等
*/
int CTcp::operator == (int p_iSockfd) const
{
return (m_nSock == p_iSockfd);
}
/*取出socket句柄
入参:无
出参:取出的socket句柄
*/
int CTcp::GetHandle () const
{
return m_nSock;
}
/*创建socket
入参:无
出参:1: 成功 ; 0: 失败
*/
int CTcp::Open ()
{
assert (m_nSock == -1);
//获取tcp套接字
m_nSock = socket (AF_INET, SOCK_STREAM, 0);
return (m_nSock != -1);
}
/*关闭socket
入参:无
出参:1: 成功 ; 0: 失败
*/
int CTcp::Close ()
{
if (m_nSock != -1)
{
close (m_nSock);
m_nSock = -1;
}
return 1;
}
/*连接(未设置超时),默认为阻塞IO
入参:pHost - IP地址或主机名
nPort - 端口
出参:1: 成功 ; 0: 失败
*/
int CTcp::Connect (const char *p_szHost, int p_iPort) const
{
assert ((m_nSock != -1) && p_szHost && (p_iPort > 0));
struct sockaddr_in addr;
struct hostent *phe = NULL;
memset ((void*)&addr, 0, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons (p_iPort);
if ((addr.sin_addr.s_addr = inet_addr (p_szHost)) == -1)
{
if ((phe = gethostbyname (p_szHost)) == NULL)
return 0;
memcpy ((char *)&addr.sin_addr, phe->h_addr, phe->h_length);
}
return (connect (m_nSock, (struct sockaddr *)&addr, sizeof (addr)) == 0);
}
/*绑定
入参:pIP - IP地址
nPort - 端口
出参:1: 成功 ; 0: 失败
*/
int CTcp::Bind (const char *pIP, int nPort) const
{
assert ((m_nSock != -1) && (nPort > 0));
struct sockaddr_in addr;
struct hostent *phe = NULL;
int opt=1;
if (setsockopt (m_nSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)) == -1)
{
return 0;
}
memset (&addr, 0, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons (nPort);
if (!pIP)
{
addr.sin_addr.s_addr = htonl (INADDR_ANY);
}
else
{
if ((addr.sin_addr.s_addr = inet_addr (pIP)) == -1)
{
if ((phe = gethostbyname (pIP)) == NULL)
return 0;
memcpy ((char *)&addr.sin_addr, phe->h_addr, phe->h_length);
}
}
return (bind (m_nSock, (struct sockaddr *)&addr, sizeof (addr)) == 0);
}
/*监听
入参:nNum - 监听数目
出参:1: 成功 ; 0: 失败
*/
int CTcp::Listen (int nNum) const
{
assert ((m_nSock != -1) && (nNum > 0));
return (listen (m_nSock, nNum) == 0);
}
/*接受连接
入参:无
出参:其他: 连接套接字句柄 ; -1: 失败
*/
int CTcp::Accept () const
{
assert (m_nSock != -1);
return (accept (m_nSock, (struct sockaddr *)NULL, NULL));
}
/*服务端接收数据
入参:pBuf - 接收缓存
nCount - 需接收字节数
出参:实际接收字节数 ,如果接收失败,返回负数,如果对方关闭,返回0
*/
int CTcp::Recv(int nFd, char* buf, int nBufLen)
{
assert(nFd != -1 );
return recv(nFd, buf, nBufLen, 0);
}
/*客户端接收数据
入参:pBuf - 接收缓存
nCount - 需接收字节数
nMicsec - socket超时值,单位:微妙,缺省:500000微妙
出参:实际接收字节数 ,如果接收失败,返回负数,如果对方关闭,返回0
*/
int CTcp::Recv (void *pBuf, int nCount, int nMicsec) const
{
assert ((m_nSock != -1) && pBuf && (nCount > 0));
int sn = 0, rn = 0;
struct timeval tvlTime;
fd_set rdfdset;
if (nMicsec >= 0)
{
tvlTime.tv_sec = nMicsec / 1000000;
tvlTime.tv_usec = abs (nMicsec - tvlTime.tv_sec * 1000000);
}
FD_ZERO (&rdfdset);
FD_SET (m_nSock, &rdfdset);
if (nMicsec > 0)
sn = select (m_nSock + 1, &rdfdset, NULL, NULL, &tvlTime);
else
sn = select (m_nSock + 1, &rdfdset, NULL, NULL, NULL);
switch (sn)
{
case -1:
return SOCK_ERROR;
case 0:
return SOCK_TIMEOUT;
}
if ((rn = read (m_nSock, pBuf, nCount)) < 0)
return SOCK_ERROR;
return rn;
}
/*服务端发送数据
入参:pBuf - 发送缓存
nCount - 需发送字节数
出参:实际发送字节数 ,如果发送失败,返回负数
*/
int CTcp::Send(int nFd, char* buf, int nBufLen)
{
assert(nFd != -1 );
return send(nFd, buf, nBufLen, 0);
}
/*客户端发送数据
入参:pBuf - 发送缓存
nCount - 需发送字节数
nMicsec - socket超时值,单位:微妙,缺省:500000微妙
出参:实际发送字节数 ,如果发送失败,返回负数
*/
int CTcp::Send (const void *pBuf, int nCount, int nMicsec) const
{
assert ((m_nSock != -1) && pBuf && (nCount > 0));
int sn = 0, wn = 0;
struct timeval tvlTime;
fd_set wtfdset;
if (nMicsec >= 0)
{
tvlTime.tv_sec = nMicsec / 1000000;
tvlTime.tv_usec = abs (nMicsec - tvlTime.tv_sec * 1000000);
}
FD_ZERO (&wtfdset);
FD_SET (m_nSock, &wtfdset);
if (nMicsec >= 0)
sn = select (m_nSock + 1, NULL, &wtfdset, NULL, &tvlTime);
else
sn = select (m_nSock + 1, NULL, &wtfdset, NULL, NULL);
switch (sn)
{
case -1:
return SOCK_ERROR;
case 0:
return SOCK_TIMEOUT;
}
if ((wn = send (m_nSock, pBuf, nCount, 0)) <= 0)
return SOCK_ERROR;
return wn;
}
void CTcp::Close (int nFd)
{
if (nFd != -1 )
{
close(nFd);
nFd = -1;
}
}
将该类编译成动态库:
g++ -g -c SxTcp.cpp -fPIC
g++ -g -o libnetwork.so SxTcp.o -shared
2.2 利用类CTcp实现基本服务端和客户端
//服务端TestServer.cpp
//TestServer.cpp
#include <stdio.h>
#include <string.h>
#include "../../network/SxTcp.h"
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
}
return 0;
}
//客户端
//TestClient.cpp
#include <stdio.h>
#include <iostream>
#include <string.h>
#include "../../network/SxTcp.h"
using namespace std;
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Connect("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket connect failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
cout << "please input some string:";
cin >> buf;
iRet = tcp.Send(buf, strlen(buf));
if (iRet < 0 && errno != EAGAIN)
{
perror("send failed");
return -1;
}
else if(iRet == 0)
{
perror("connect is closed");
return -1;
}
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(buf, sizeof(buf));
if (iRet < 0 && errno != EAGAIN)
{
perror("recv failed");
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
}
return 0;
}
分别编译服务端和客户端,然后发送数据,会发现客户端发送完第一次后,再第二次循环中会报recv failed。 这是因为服务端阻塞在accept了,没办法第二次接收和发送数据,那么客户端超时以后就会报错,返回负数,导致客户端退出。 当然也可以在服务端的recv和send外再加一个循环,如下:
//TestServer.cpp
#include <stdio.h>
#include <string.h>
#include "../../network/SxTcp.h"
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
while(1){
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
}
}
return 0;
}
但很显然,这样就没办法接收到第二个连接了,那么怎么解决呢?往下面看。
3. 网络编程模式
上面的虽然可以实现多个客户端访问,但是仍然是阻塞模式(即一个客户访问的时候会阻塞不让另外的客户访问)。解决办法有三种,分别是多进程、多线程、异步IO。
3.1 多进程
//因为开销比较大,所以不常用
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "../../network/SxTcp.h"
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
if (fork() == 0)
{
while(1){
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
}
}
else
{
tcp.Close(iFd);
}
}
return 0;
}
3.2 多线程
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <thread>
#include "../../network/SxTcp.h"
CTcp tcp;
void ReadThread(void* arg)
{
int *pFd = (int*)arg;
int iFd = *pFd;
char buf[128] = {0};
int iRet = 0;
while(1){
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
break;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
break;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
break;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
break;
}
}
}
int main()
{
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
std::thread t_read(ReadThread, (void*)&iFd);
t_read.detach();
}
return 0;
}
3.3 异步IO
异步其实就是epoll和select模式,可以看另外的两篇专门讲epoll和select的文章。
4. 使用UDP编程
4.1 UDP协议
4.1.1 概述
UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。
4.1.2 Udp数据包头格式
源端口占用16bit,表示应用程序通过哪个端口来发送数据包; 目的端口占用16bit,表示数据包发送给对方应用程序的哪个端口; 长度占用16bit,表示包含头部在内的udp数据包的长度; 校验占用16bit,用来检查数据包是否存在差错;
4.1.3 udp基本通信流程及函数
UDP通信流程图如下: 服务端:socket---bind---recvfrom---sendto---close 客户端:socket----------sendto---recvfrom---close sendto()函数原型: int sendto(int sockfd, const void msg,int len,unsigned int flags,const struct sockaddr to, int tolen); 该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。 recvfrom()函数原型: int recvfrom(int sockfd,void buf,int len,unsigned int flags,struct sockaddr from,int *fromlen); from是一个struct sockaddr类型的变量,该变量保存连接机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。 注意:socket编程还提供了一对函数sendmsg/recvmsg用于读写数据,该对函数既可用于tcp报文,也可用于udp报文,是通用的。
4.2 UDP编程实现
Example:
//UDP的基本操作
//服务器端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
main()
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}
char buf[512] = {0};
while(1)
{
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
sendto(sfd, "world", 6, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
}
close(sfd);
}
//客户端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in toaddr;
bzero(&toaddr, sizeof(toaddr));
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(atoi(argv[2])); //此处的端口号要跟服务器一样
toaddr.sin_addr.s_addr = inet_addr(argv[1]); //此处为服务器的ip
sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));
char buf[512] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
close(sfd);
}
Example2:
//UDP发送文件,先发文件大小,再发文件内容
//服务器端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
main()
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}
char buf[512] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
FILE* fp = fopen("1.txt","rb");
struct stat st; //用于获取文件内容的大小
stat("1.txt", &st);
int filelen = st.st_size;
sendto(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
while(!feof(fp)) //表示没有到文件尾
{
int len = fread(buf,1,sizeof(buf),fp);
sendto(sfd, buf, len, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
}
close(sfd);
}
//客户端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFSIZE 512
int main(int argc, char* argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in toaddr;
bzero(&toaddr, sizeof(toaddr));
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(atoi(argv[2]));
toaddr.sin_addr.s_addr = inet_addr(argv[1]);
sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));
char buf[BUFSIZE] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
int filelen = 0; //用于保存文件长度
FILE* fp = fopen("2.txt","w+b");
//接收文件的长度
recvfrom(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
printf("the length of file is %d\n",filelen);
printf("Create a new file!\n");
printf("begin to reveive file content!\n");
//接收文件的内容
while(1)
{
int len = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
if(len < BUFSIZE)
//如果接收的长度小于BUFSIZE,则表示最后一次接收,此时要用break退出循环
{
fwrite(buf,sizeof(char),len,fp);
break;
}
fwrite(buf,sizeof(char),len,fp);
}
printf("receive file finished!\n");
close(sfd);
}
5. 协议的选择
- 对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送;
- TCP协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用;
- 由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷;
linux网络编程系列(五)--setsockopt的常用选项
1. 函数原型
#include <sys/types.h >
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd:标识一个套接口的描述字
- level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
- optname:需设置的选项,而有部分选项需在listen/connect调用前设置才有效,这部分选项如下:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY
- optval:指针,指向存放选项值的缓冲区
- optlen:optval缓冲区长度
2. 使用场景
(1)如果在已经处于 ESTABLISHED状态下的socket(一般由端口号和标志符区分)调用close(socket)(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
int reuse=1;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));
注意:必须在调用bind函数之前设置SO_REUSEADDR选项。
(2)如果要已经处于连接状态的soket在调用close(socket)后强制关闭,不经历TIME_WAIT的过程:
int reuse=0;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));
(3)在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000; // 1秒
// 发送时限
setsockopt(socket,SOL_S0CKET, SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
// 接收时限
setsockopt(socket,SOL_S0CKET, SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
(4)在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步),系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:
// 接收缓冲区
int nRecvBuf=32*1024; // 设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
// 发送缓冲区
int nSendBuf=32*1024; // 设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
注意:并不是说你设置的多大,系统就会设置多大,系统一般会将我们设置的缓冲区大小加倍,并且不得小于tcp的接收缓冲区和发送缓冲区设置的默认最小值。 注意:TCP有发送缓冲区和接收缓冲区,但是UDP因为是不可靠的,它没有确认重传机制,不保存应用程序数据的副本,所以是没有发送缓冲区的,但是UDP有接收缓冲区。
(5)如果在发送数据时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能:
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(int));
(6)同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
(7)一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
int bBroadcast = 1;
setsockopt(s, SOL_SOCKET, SO_BROADCAST, (const char*)&bBroadcast, sizeof(int));
(8)设置存活检测
int opt = 1;
if (setsockopt (m_nSock, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(int)) == -1)
{
return 0;
}
(9)延迟接收 实际上就是当接收到第一个数据之后,才会创建连接。对于像http这类非交互式的服务器,这个很有意义,可以防御空连接攻击。
int val = 5;
setsockopt(fd, SOL_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val));
打开这个功能后,内核在val时间之类还没有收到数据,不会继续唤醒进程,而是直接丢弃连接。 从三次握手上讲,就是设置这个状态之后,就算完成了三次握手,服务器socket状态也不是ESTABLISHED,而依然是 SYN_RCVD,不会去接收数据。