LINUX网络编程基础知识
TCP/IP协议概述
协议protocol:通信双方必须遵循的规矩 由iso规定,
osi参考模型:(应-表-会-传-网-数-物)
- 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层
tcp/ip模型4层:
- 应用层{http超文本传输协议 ftp文件传输协议 telnet远程登录 ssh安全外壳协议 stmp简单邮件发送 pop3收邮件}
- 传输层{tcp传输控制协议,udp用户数据包协议}
- 网络层{ip网际互联协议 icmp网络控制消息协议 igmp网络组管理协议}
- 链路层{arp地址转换协议,rarp反向地址转换协议,mpls多协议标签交换}
TCP协议:传输控制协议 面向连接的协议 能保证传输安全可靠 速度慢(有3次握手)
UDP协议:用户数据包协议 非面向连接 速度快 不可靠
通常是ip地址后面跟上端口号:ip用来定位主机 port区别应用(进程)
http的端口号80 ssh—>22 telnet—>23 ftp—>21 用户自己定义的通常要大于1024
OSI参考模型及TCP/IP参考模型
TCP/IP协议族的每一层的作用:
- 链路层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。
- 网络层:负责将数据帧封装成IP数据报,并运行必要的路由算法。
- 传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。
- 应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。
TCP/IP协议族的每一层协议的相关注解:
- ARP:(地址转换协议)用于获得同一物理网络中的硬件主机地址。
- MPLS:(多协议标签交换)很有发展前景的下一代网络协议。
- IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。
- ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。
- IGMP:(网络组管理协议)被IP主机用来向本地多路广播路由器报告主机组成员的协议。
- TCP:(传输控制协议)为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到相应的应用程序。
- UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。
TCP协议
概述
TCP是TCP/IP体系中面向连接的运输层协议,它提供全双工和可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。
首先,TCP要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。
其次,TCP采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。
最后,采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP采用可变长的滑动窗口,使得发送端与接收端可根据自己的CPU和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。
三次握手协议
在利用TCP实现源主机和目的主机通信时,目的主机必须同意,否则TCP连接无法建立。为了确保TCP连接的成功建立,TCP采用了一种称为三次握手的方式,三次握手方式使得“序号/确认号”系统能够正常工作,从而使它们的序号达成同步。如果三次握手成功,则连接建立成功,可以开始传送数据信息。
其三次握手分别为:
- 源主机A的TCP向主机B发送连接请求报文段,其首部中的SYN(同步)标志位应置为1,表示想跟目标主机B建立连接,进行通信,并发送一个同步序列号X(例:SEQ=100)进行同步,表明在后面传送数据时的第一个数据字节的序号为X+1(即101)。
- 目标主机B的TCP收到连接请求报文段后,如同意,则发回确认。再确认报中应将ACK位和SYN位置为1.确认号为X+1,同时也为自己选择一个序号Y。
- 源主机A的TCP收到目标主机B的确认后要想目标主机B给出确认。其ACK置为1,确认号为Y+1,而自己的序号为X+1。TCP的标准规定,SYN置1的报文段要消耗掉一个序号。
运行客户进程的源主机A的TCP通知上层应用进程,连接已经建立。当源主机A向目标主机B发送第一个数据报文段时,其序号仍为X+1,因为前一个确认报文段并不消耗序号。
当运行服务进程的目标主机B的TCP收到源主机A的确认后,也通知其上层应用进程,连接已经建立。至此建立了一个全双工的连接。
三次握手:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。
TCP数据报头
TCP头信息
- 源端口、目的端口:16位长。标识出远端和本地的端口号。
- 序号:32位长。标识发送的数据报的顺序。
- 确认号:32位长。希望收到的下一个数据报的序列号。
- TCP头长:4位长。表明TCP头中包含多少个32位字。
- 6位未用。
- ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据报不包含确认信息,确认字段被省略。
- PSH:表示是带有PUSH标志的数据。接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才发送。
- RST:用于复位由于主机崩溃或其他原因而出现的错误的连接。还可以用于拒绝非法的数据报或拒绝连接请求。
- SYN:用于建立连接。
- FIN:用于释放连接。
- 窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
- 校验和:16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。
- 可选项:0个或多个32位字。包括最大TCP载荷,窗口比例、选择重复数据报等选项。
UDP协议
概述
UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。
协议的选择
对数据可靠性的要求
对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送。
应用的实时性
TCP协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。
网络的可靠性
由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷。
网络相关概念
套接口的概念:
套接口,也叫“套接字”。是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户。它是网络进程的ID。网络通信,归根到底还是进程间的通信(不同计算机上的进程间通信)。在网络中,每一个节点(计算机或路由)都有一个网络地址,也就是IP地址。两个进程通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,也就是端口号(PORT)。在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程之间是一一对应关系。所以,使用端口号和网络地址的组合可以唯一的确定整个网络中的一个网络进程。
例如,如网络中某一台计算机的IP为10.92.20.160,操作系统分配给计算机中某一应用程序进程的端口号为1500,则此时 10.92.20.160 1500就构成了一个套接口。
端口号的概念:
在网络技术中,端口大致有两种意思:一是物理意义上的端口,如集线器、交换机、路由器等用于连接其他网络设备的接口。二是指TCP/IP协议中的端口,端口号的范围从0~65535,一类是由互联网指派名字和号码公司ICANN负责分配给一些常用的应用程序固定使用的“周知的端口”,其值一般为0~1023.例如http的端口号是80,ftp为21,ssh为22,telnet为23等。还有一类是用户自己定义的,通常是大于1024的整型值。
ip地址的表示:
通常用户在表达IP地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在通常使用的socket编程中使用的则是二进制值,这就需要将这两个数值进行转换。
ipv4地址:32bit, 4字节,通常采用点分十进制记法。
例如对于:10000000 00001011 00000011 00011111
点分十进制表示为:128.11.3.31
socket概念
Linux中的网络编程是通过socket接口来进行的。socket是一种特殊的I/O接口,它也是一种文件描述符。它是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的;
socket类型
(1)流式socket(SOCK_STREAM) 用于TCP通信
流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
(2)数据报socket(SOCK_DGRAM) 用于UDP通信
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。
(3)原始socket (SOCK_RAW) 用于新的网络协议实现的测试等
原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
socket信息数据结构
1 | struct sockaddr |
头文件
数据存储优先顺序的转换
计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式)。内存的低地址存储数据的低字节,高地址存储数据的高字节的方式叫小端模式。内存的高地址存储数据的低字节,低地址存储数据高字节的方式称为大端模式。
eg:对于内存中存放的数0x12345678来说
- 如果是采用大端模式存放的,则其真实的数是:0x12345678
- 如果是采用小端模式存放的,则其真实的数是:0x78563412
如果称某个系统所采用的字节序为主机字节序,则它可能是小端模式的,也可能是大端模式的。而端口号和IP地址都是以网络字节序存储的,不是主机字节序,网络字节序都是大端模式。要把主机字节序和网络字节序相互对应起来,需要对这两个字节存储优先顺序进行相互转化。这里用到四个函数:htons(),ntohs(),htonl()和ntohl().这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。
地址格式转化
通常用户在表达地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在通常使用的socket编程中使用的则是32位的网络字节序的二进制值,这就需要将这两个数值进行转换。这里在Ipv4中用到的函数有inet_aton()
、inet_addr()
和inet_ntoa()
,而IPV4和Ipv6兼容的函数有inet_pton()
和inet_ntop()
。
IPv4的函数原型:1
2
3
4
5
6
7
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位二进制的网络字节序地址。
IPv4和IPv6的函数原型:1
2
3
4
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表示表示转换之后的长度(字符串的长度)。
Example:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main()
{
char ip[] = "192.168.0.101";
struct in_addr myaddr;
/* inet_aton */
int iRet = inet_aton(ip, &myaddr);
printf("%x\n", myaddr.s_addr);
/* inet_addr */
printf("%x\n", inet_addr(ip));
/* inet_pton */
iRet = inet_pton(AF_INET, ip, &myaddr);
printf("%x\n", myaddr.s_addr);
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;
}
名字地址转化
通常,人们在使用过程中都不愿意记忆冗长的IP地址,尤其到Ipv6时,地址长度多达128位,那时就更加不可能一次性记忆那么长的IP地址了。因此,使用主机名或域名将会是很好的选择。主机名与域名的区别:主机名通常在局域网里面使用,通过/etc/hosts文件,主机名可以解析到对应的ip;域名通常是再internet上使用。
众所周知,百度的域名为:www.baidu.com,而这个域名其实对应了一个百度公司的IP地址,那么百度公司的IP地址是多少呢?我们可以利用ping www.baidu.com来得到百度公司的ip地址,如图。那么,系统是如何将www.baidu.com 这个域名转化为IP地址220.181.111.148的呢?
在linux中,有一些函数可以实现主机名和地址的转化,最常见的有gethostbyname()
、gethostbyaddr()
等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()
是将主机名转化为IP地址,gethostbyaddr()
则是逆操作,是将IP地址转化为主机名。
函数原型:1
2
3
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);
结构体:1
2
3
4
5
6
7
8
9
10struct 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地址列表*/
}
函数gethostbyname()
:用于将域名(www.baidu.com)或主机名转换为IP地址。参数hostname指向存放域名或主机名的字符串。
函数gethostbyaddr()
:用于将IP地址转换为域名或主机名。参数addr是一个IP地址,此时这个ip地址不是普通的字符串,而是要通过函数inet_aton()转换。len为IP地址的长度,AF_INET为4。family可用AF_INET:Ipv4或AF_INET6:Ipv6。
Example1:将百度的www.baidu.com 转换为ip地址1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int main(int argc, char **argv)
{
char *ptr, **pptr;
struct hostent *hptr;
char str[32] = {'\0'};
/* 取得命令后第一个参数,即要解析的域名或主机名 */
ptr = argv[1]; //如www.baidu.com
/* 调用gethostbyname()。结果存在hptr结构中 */
if((hptr = gethostbyname(ptr)) == NULL)
{
printf(" gethostbyname error for host:%s\n", ptr);
return 0;
}
/* 将主机的规范名打出来 */
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;
}
编译运行1
2
3
4
5
6
7
8
9
10
11
12
13#gcc test.c
#./a.out www.baidu.com
official hostname:www.a.shifen.com
alias:www.baidu.com
address: 220.181.111.148
……
first address: 220.181.111.148
socket编程
使用TCP协议的流程图
TCP通信的基本步骤如下:
服务端:socket—-bind—-listen—-while(1){—-accept—-recv—-send—-close—-}—-close
客户端:socket——————————————connect—-send—-recv————————-close
服务器端
头文件包含:1
2
3
4
5
6
7
8
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所使用的传输协议编号。通常为0.
返回值:成功则返回套接口描述符,失败返回-1。
常用实例:1
2int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){perror("socket");exit(-1);}
bind函数
用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。
原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
参数:
- sockfd 为前面socket的返回值。
- my_addr 为结构体指针变量
对于不同的socket domain定义了一个通用的数据结构1
2
3
4
5struct sockaddr //此结构体不常用
{
unsigned short int sa_family; //调用socket()时的domain参数,即AF_INET值。
char sa_data[14]; //最多使用14个字符长度
};
此sockaddr结构会因使用不同的socket domain而有不同结构定义, 例如使用AF_INET domain,其socketaddr
结构定义便为1
2
3
4
5
6
7
8
9struct 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; }; // addrlen sockaddr的结构体长度。通常是计算sizeof(struct sockaddr);
返回值:成功则返回0,失败返回-1
常用实例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct 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地址。)
listen函数
使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
原型:int listen(int sockfd, int backlog);
参数:
- sockfd 为前面socket的返回值.即sfd
- backlog 指定同时能处理的最大连接要求,通常为10或者5。 最大值可设至128
返回值:成功则返回0,失败返回-1
常用实例:1
2if(listen(sfd, 10) == -1)
{ perror("listen");close(sfd);exit(-1); }
accept函数
接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求。(也就是说,类似于移动营业厅,如果有客户打电话给10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,也就是说,后面的所有操作,此时已经于服务器没有关系,而是话务员跟客户的交流。对应过来,客户请求连接我们的服务器,我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用accept函数产生一个新的套接字,然后用这个新的套接字跟我们的客户进行收发数据。也就是说,服务器跟一个客户端连接成功,会有两个套接字。)
原型:int accept(int s,struct sockaddr * addr,int * addrlen);
参数:
- s 为前面socket的返回值.即sfd
- addr 为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
- addrlen 表示结构体的长度,为整型指针
返回值:成功则返回新的socket处理代码new_fd,失败返回-1
常用实例:1
2
3
4
5
6
7
8struct 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));
recv函数
用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
参数:
- sockfd 为前面accept的返回值.即new_fd,也就是新的套接字。
- buf 表示缓冲区
- len 表示缓冲区的长度
- flags 通常为0
返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1
常用实例:1
2
3
4
5char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{perror("recv");close(new_fd);close(sfd);exit(-1);}
puts(buf);
send函数
用新的套接字发送数据给指定的远端主机
原型:int send(int s,const void * msg,int len,unsigned int flags);
参数:
- s 为前面accept的返回值.即new_fd
- msg 一般为常量字符串
- len 表示长度
- flags 通常为0
返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1
常用实例:1
2if(send(new_fd, "hello", 6, 0) == -1)
{perror("send");close(new_fd);close(sfd);exit(-1);}
close函数
当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源
原型:int close(int fd);
参数:
- fd 为前面的sfd,new_fd
返回值:若文件顺利关闭则返回0,发生错误时返回-1
常用实例:1
close(new_fd);
客户端
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
常用实例:1
2
3
4
5
6
7struct 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来查看服务端的状况。
Example:将一些通用的代码全部封装起来,以后要用直接调用函数即可。如下:
通用网络封装代码头文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tcp_net_socket.h
extern int tcp_init(const char* ip,int port);
extern int tcp_accept(int sfd);
extern int tcp_connect(const char* ip,int port);
extern void signalhandler(void);
具体的通用函数封装如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86tcp_net_socket.c
int tcp_init(const char* ip, int port) //用于初始化操作
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);//首先创建一个socket,向系统申请
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);//或INADDR_ANY
if(bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)
//将新的socket与制定的ip、port绑定
{
perror("bind");
close(sfd);
exit(-1);
}
if(listen(sfd, 10) == -1)//监听它,并设置其允许最大的连接数为10个
{
perror("listen");
close(sfd);
exit(-1);
}
return sfd;
}
int tcp_accept(int sfd) //用于服务端的接收
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
//sfd接受客户端连接,并创建新的socket为new_fd,将请求连接的客户端的ip、port保存在结构体clientaddr中
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));
return new_fd;
}
int tcp_connect(const char* ip, int port) //用于客户端的连接
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);//向系统注册申请新的socket
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);
if(connect(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)
//将sfd连接至制定的服务器网络地址serveraddr
{
perror("connect");
close(sfd);
exit(-1);
}
return sfd;
}
void signalhandler(void) //用于信号处理,让服务端在按下Ctrl+c或Ctrl+\的时候不会退出
{
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet,SIGINT);
sigaddset(&sigSet,SIGQUIT);
sigprocmask(SIG_BLOCK,&sigSet,NULL);
}
服务器端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40tcp_net_server.c
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./servertcp ip port\n");
exit(-1);
}
signalhandler();
int sfd = tcp_init(argv[1], atoi(argv[2])); //或int sfd = tcp_init("192.168.0.164", 8888);
while(1) //用while循环表示可以与多个客户端接收和发送,但仍是阻塞模式的
{
int cfd = tcp_accept(sfd);
char buf[512] = {0};
if(recv(cfd, buf, sizeof(buf), 0) == -1)//从cfd客户端接收数据存于buf中
{
perror("recv");
close(cfd);
close(sfd);
exit(-1);
}
puts(buf);
if(send(cfd, "hello world", 12, 0) == -1)//从buf中取向cfd客户端发送数据
{
perror("send");
close(cfd);
close(sfd);
exit(-1);
}
close(cfd);
}
close(sfd);
}
客户端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18tcp_net_client.c
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./clienttcp ip port\n");
exit(-1);
}
int sfd = tcp_connect(argv[1],atoi(argv[2]));
char buf[512] = {0};
send(sfd, "hello", 6, 0); //向sfd服务端发送数据
recv(sfd, buf, sizeof(buf), 0); //从sfd服务端接收数据
puts(buf);
close(sfd);
}
1 | #gcc –o tcp_net_server tcp_net_server.c tcp_net_socket.c |
上面的虽然可以实现多个客户端访问,但是仍然是阻塞模式(即一个客户访问的时候会阻塞不让另外的客户访问)。解决办法有:
多进程(因为开销比较大,所以不常用)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{
int sfd = tcp_init("192.168.0.101", 8888);
while(1)
{
int cfd = tcp_accept(sfd);
if(fork() == 0)
{
send(cfd, "hello", 6, 0);
sleep(10);
close(cfd);
}
else
{
close(cfd);
}
}
close(sfd);
return 0;
}
多线程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void* pthfunc(void* arg)
{
int cfd = (int)arg;
send(cfd, "hello", 6, 0);
sleep(10);
close(cfd);
}
int main()
{
int sfd = tcp_init("192.168.0.101", 8888);
pthread_t pthid = 0;
while(1)
{
int cfd = tcp_accept(sfd);
pthread_create(&pthid, NULL, pthfunc, (void*)cfd);
}
close(sfd);
return 0;
}
// 备注 读写大容量的文件时,通过下面的方法效率很高
ssize_t readn(int fd, char *buf, int size)//读大量内容
{
char *pbuf = buf;
int total ,nread;
for(total = 0; total < size; )
{
nread=read(fd,pbuf,size-total);
if(nread==0)
return total;
if(nread == -1)
{
if(errno == EINTR)
continue;
else
return -1;
}
total += nread;
pbuf += nread;
}
return total;
}
ssize_t writen(int fd, char *buf, int size)//写大量内容
{
char *pbuf=buf;
int total ,nwrite;
for(total = 0; total < size; )
{
nwrite=write(fd,pbuf,size-total);
if( nwrite <= 0 )
{
if( nwrite == -1 && errno == EINTR )
continue;
else
return -1;
}
total += nwrite;
pbuf += nwrite;
}
return total;
}
调用fcntl将sockfd设置为非阻塞模式。(不常见)1
2
3
4
5
6
7
8
……
sockfd = socket(AF_INET,SOCK_STREAM,0);
iflags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd,F_SETFL,O_NONBLOCK | iflags);
……
多路选择select1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
main()
{
int sfd = tcp_init("192.168.0.164", 8888);
int fd = 0;
char buf[512] = {0};
fd_set rdset;
while(1)
{
FD_ZERO(&rdset);
FD_SET(sfd,&rdset);
if(select(MAXCLIENT + 1, &rdset, NULL, NULL, NULL) < 0)
continue;
for(fd = 0; fd < MAXCLIENT; fd++)
{
if(FD_ISSET(fd,&rdset))
{
if(fd == sfd)
{
int cfd = tcp_accept(sfd);
FD_SET(cfd,&rdset);
//……
}
else
{
bzero(buf, sizeof(buf));
recv(fd, buf, sizeof(buf), 0);
puts(buf);
send(fd, "java", 5, 0);
//FD_CLR(fd, &rdset);
close(fd);
}
}
}
}
close(sfd);
}
使用UDP协议的流程图
UDP通信流程图如下:
服务端:socket—-bind—-recvfrom—-sendto—-close
客户端:socket—————sendto—-recvfrom—-close
sendto()
函数原型:1
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()
函数原型:1
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。
Example:UDP的基本操作
服务器端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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);
}
客户端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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);
}
Example:UDP发送文件 先发文件大小 再发文件内容
服务器端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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);
}
客户端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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);
}
设置套接口的选项setsockopt的用法
函数原型:1
2
3
4
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:需设置的选项
- optval:指针,指向存放选项值的缓冲区
- optlen:optval缓冲区长度
全部都必须要放在bind之前,另外通常是用于UDP的。
如果在已经处于 ESTABLISHED状态下的socket(一般由端口号和标志符区分)调用closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:1
2int reuse=1;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));
如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历TIME_WAIT的过程:1
2int reuse=0;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));
在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:1
2
3
4
5int nNetTimeout=1000; // 1秒
// 发送时限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
// 接收时限
setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步),系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:1
2
3
4
5
6// 接收缓冲区
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));
如果在发送数据时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能:1
2int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(int));
同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):1
2int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:1
2int bBroadcast = 1;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(int));
单播、广播、组播(多播)
多播广播是用于建立分步式系统:例如网络游戏、ICQ聊天构建、远程视频会议系统的重要工具。使用多播广播的程序和UDP的单播程序相似。区别在于多播广播程序使用特殊的IP地址。
对于单播而言,单播用于两个主机之间的端对端通信。
对于广播而言,广播用于一个主机对整个局域网上所有主机上的数据通信。广播只能用于客户机向服务器广播,因为客户机要指明广播的IP地址“192.168.0.255”和广播的端口号。服务器端bing的时候,绑定的端口号要跟广播的端口号是同一个。这样才能收到广播消息。实例请参考《udp_广播》。
对于多播而言,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。单播和广播是两个极端,要么对一个主机进行通信,要么对整个局域网上的主机进行通信。实际情况下,经常需要对一组特定的主机进行通信,而不是整个局域网上的所有主机,这就是多播的用途。例如,我们通常所说的讨论组。IPv4多播地址采用D类IP地址确定多播的组。在Internet中,多播地址范围是从224.0.0.0到234.255.255.255。
多播的程序设计也要使用setsockopt()函数和getsockopt()函数来实现。其中对于setsockopt的第二个参数level不再是SOL_SOCKET,而是IPPROTO_IP;而且第三个参数optname常见的选项有:
IP_ADD_MEMBERSHIP
:在指定接口上加入组播组IP_DROP_MEMBERSHIP
:退出组播组
选项IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP加入或者退出一个组播组,通过选项IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP,对一个结构struct ip_mreq类型的变量进行控制。
struct ip_mreq
原型如下:1
2
3
4
5struct ip_mreq
{
struct in_addr imr_multiaddr; /*加入或者退出的多播组IP地址*/
struct in_addr imr_interface; /*加入或者退出的网络接口IP地址,本机IP*/
};
选项IP_ADD_MEMBERSHIP用于加入某个多播组,之后就可以向这个多播组发送数据或者从多播组接收数据。此选项的值为mreq结构,成员imr_multiaddr是需要加入的多播组IP地址,成员imr_interface是本机需要加入多播组的网络接口IP地址。例如:1
2
3
4
5
6
7
8
9struct ip_mreq mreq;
memset(&mreq, 0, sizeof(struct ip_mreq));
mreq.imr_interface.s_addr = INADDR_ANY;
mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1");
if(-1 == setsockopt(sfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)))
{
perror("setsockopt");
exit(-1);
}
接下来再绑定组播的port号(如65000),就可以接收组播消息了。
选项IP_ADD_MEMBERSHIP每次只能加入一个网络接口的IP地址到多播组,但并不是一个多播组仅允许一个主机IP地址加入,可以多次调用IP_ADD_MEMBERSHIP选项来实现多个IP地址加入同一个广播组,或者同一个IP地址加入多个广播组。
选项IP_DROP_MEMBERSHIP用于从一个多播组中退出。例如:1
2
3
4
5if(-1 == setsockopt(sfd, IPPROTP_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)))
{
perror("setsockopt");
exit(-1);
}