二、网络编程基础
1、套接字概述
套接字就是网络编程的ID。网络通信,归根到底还是进程间的通信(不同计算机上的进程间的通信)。在网络中,每一个节点(计算机或路由器)都有一个网络地址,也就是IP地址,两个进程通信时,首先要确定各自所在网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中哪一个进程通信,因此套接口中还需要有其他的信息,也就是端口号(port)。在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程之间是一一对应的关系。所以,使用端口号和网络地址的组合就能唯一确定整个网络中的一个网络进程。
把网络地址和端口号信息放在一个结构体中,也就是套接口地址结构,大多数的套接口函数都需要一个指向套接口地址结构的指针作为参数,并以此来传递地址信息。每个协议族都定义了它自己的套接口地址结构,套接字地址结构都以"sockaddr_” 开头,并以每个协议族名中两个字母作为结尾。
下面是socket所在位置:
可以看到套接口有3种类型:
1)流式套接字(SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信刘,保证数据传输的可靠性和按序收发。TCP通信使用的就是流式套接字。
2)数据包套接字(SOCK_DGRAM)
数据报套接字实现了一种不可靠、无连接的服务。数据通过相互独立的报文进行传输,是无序的,并且不保证可靠的传输。UDP通信使用的就是数据报套接字。
3)原始套接字(SOCK_RAW)
原始套接字允许对底层协议(如IP或ICMP)进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
2、端口号
这里的端口号是逻辑意义上的端口,一般是指TCP/IP 协议中的端口,端口号的范围为0~65535,比如用于浏览网页服务(HTTP协议)的80端口,用于FTP服务的21端口等。其中, 0 到1023 一般被系统程序所使用。
那么TCP/IP协议中的端口指的是什么呢?举个例子,如果IP地址唯一指定了地球上某个地理位置的一间房子,端口号就是出入这间房子的门,只不过这个房子的门有65536个之多,端口是通过端口号来标记的,端口号是一个16位的整数,范围是从0~65535。
端口号只具有本地意义,即端口号只是为了标识本地计算机上的各个进程。在互联网中不同计算机的相同端口号是没有联系的。16bit 的端口号可允许有64K个端口号,这个数目对一个计算机来说是足够用的。
3、IP地址
1)IP地址的作用
IP地址用来表示网络中的一台主机。准确的说,IP地址是一台主机到一个网络的一个连接,因为现在一个主机中会有多个网卡。
一个IP地址包含两部分:网络号和主机号。其中,网络号和主机号根据子网掩码来区分。简单的说,有了源IP 和目标 IP,数据包就能在不同主机之间传输。
2)IP地址格式转换
IP地址有两种不同格式:十进制点分形式和32位二进制形式。前者是用户熟悉的形式,而后者则是网络传输中IP地址的存储方式。
这里主要介绍IPV4地址转换函数,主要有 inet_addr() 、inet_aton() 、inet_ntoa() 。前两者的功能都是将字符串转换成32位网络字节序二进制值,第三个将32位网络字节序二进制地址转换成点分十进制的字符串。
inet_addr() 函数语法如下:
所需头文件 | #include <arpa/inet.h> |
函数原型 | int inet_addr(const char *strptr); |
参数 | strptr :要转换的IP地址字符串 |
函数返回值 | 成功:32位二进制IP地址(网络字节序) 出错:-1 |
inet_aton() 函数语法如下:
所需头文件 | #include <arpa/inet.h> |
函数原型 | int inet_aton(int family, const char *src , void *drt); |
参数 | family AF_INET:IPV4协议 AF_INET6:IPV6协议 src:要转换的IP地址字符串 |
函数返回值 | 成功:32位二进制IP地址(网络字节序) 出错:-1 |
inet_ntoa() 函数语法如下:
所需头文件 | #include <arpa/inet.h> |
函数原型 | int inet_ntoa(int family, const char *src , void *dst, size_t len); |
参数 | family AF_INET:IPV4协议 AF_INET6:IPV6协议 src:要转换的二进制IP地址; |
函数返回值 | 成功:返回dst 出错:NULL |
4、字节序
字节序又称为主机字节序 Host Byte Order,HBO,是指计算机中多字节整型数据的存储方式。字节序有两种:大端(高位字节存储在低位地址,低位字节存储在高位地址)和小端(和大端序相反,PC通常采用小端模式)。
为什么需要字节序?在网络通信中,发送方和接收方有可能使用不同的字节序;
为了保证数据接受后能被正确的解析处理,统一规定:数据以高位字节优先顺序在网络上传输。因此数据在发送前和接收后都需要在主机字节序和网络字节序之间转换。
1)函数说明
字节序转换涉及4个函数:htons() 、ntohs() 、htonl() 和 ntohl() 。这里的 h 代表 host , n 代表 network , s 代表 short , l 代表 long 。通常 16bit 的IP端口号用前两个函数处理,而 IP 地址用后两个函数来转换。调用这些函数只是使其得到相应的字节序,用户不需要知道该系统的主机字节序和网络字节序是否真的相等。如果两个相同不需要转换的话,该系统的这些函数会定义成空宏。
2)函数格式
所需头文件 | #include <netinet/in.h> |
函数原型 | unit16_t htons(unit 16_t hostshort); unit32_t htonl(unit 32_t hostlong); unit16_t ntohs(unit 16_t netshort); unit32_t ntohl(unit 32_t netlong); |
函数传入值 | hostshort:主机字节序的16bit 数据 hostlong :主机字节序的32t 数据 netshort :网络字节序的16bit 数据 netlong:网络字节序的32bitt 数据 |
函数返回值 | 成功:返回转换字节序后的数值 出错:-1 |
5、TCP编程
函数说明
socket()编程的基本函数有socket() 、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等。下面先简单介绍上述函数的功能,再结合流程图具体说明
1)socket() :该函数用于创建一个套接字,同时指定协议和类型。
2)bind() :该函数将保存在相应地址结构中的地址信息与套接字进行绑定。它主要用于服务器端,客户端创建的套接字可以不绑定地址。
3)listen() :在服务端程序成功建立套接字并与地址进行绑定以后,通过调用listen() 函数将TCP连接后,该函数会返回一个新的已连接套接字。
5)connect():客户端通过该函数向服务器端的监听套接字发送连接请求。
6)send() 和 recv():这两个函数通常在TCP通信过程中用于发送和接收数据,也可用于UDP中。
7)sendto()和recvfrom() :这两个函数一般在UDP通信过程中用于发送和接受数据。当用于TCP时,后面的几个与地址有关的参数不起作用,函数作用等同于 send() 和 recv()
服务器端和客户端使用TCP的流程如下:
可以看到通信工作的大致流程如下:
1)服务器先用socket() 函数来建立一个套接口,用这个套接口完成通信的监听及数据的收发;
2)服务器用bind() 函数来绑定一个端口号和IP地址,使套接口与制定的端口号和IP地址相关联;
3)服务器调用listen()函数,使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。
4)客户机调用socket()函数建立一个套接口,设定远程IP和端口。
5)客户机调用 connect() 函数链接远程计算机指定的端口。
6)服务器调用 accept() 函数来接受远程计算机的连接请求,建立起与客户机之间的通信连接。
7)建立连接以后,客户机用write() 函数 (或send()函数)向socket() 中写入数据,也可以用 read() 函数(或recv()函数)读取服务器发送来的数据。
8)服务器用 read()函数(或recv()函数)读取客户机发送来的数据,也可以用 write() 函数(或send()函数)来发送数据。
9)完成通信以后,使用close()函数关闭socket 连接。
函数格式:
1)创建套接口 socket() 函数
其语法要点
所需头文件 | #include <sys/socket.h> |
函数原型 | int socket(int family, int type,int protocol); |
函数传入值 | family:协议族 type:套接字类型 protocol:0(原始套接字除外) |
函数返回值 | 成功:非负套接字描述符 出错:-1 |
参数family 指明协议族,取值如:
AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_LOCAL:UNIX域协议
AF_ROUTE:路由套接字
AF_KEY:密钥套接字
这里“AF”代表“Adress Family”(地址族)
types指明通信字节流类型,其取值如:
SOCK_STREAM:流式套接字(TCP方式)
SOCK_DGRAM:数据包套接字(UDP方式)
SOCK_RAM:原始套接字
2)绑定端口 bind()函数
用socket() 函数创建一个套接口后,需要使用bind 函数在这个套接口上绑定一个指定的端口号和IP地址。bind函数原型如下:
所需头文件 | #include <sys/socket.h> |
函数原型 | int bind(int sockfd, struct sockaddr *my_addr, int addrlen); |
函数传入值 | sockfd:套接字描述符 my_addr:绑定的地址 addrlen:地址长度 |
函数返回值 | 成功:0 出错:-1 |
这里my_addr是IPv4地址,IPv4 套接口地址数据结构以socketaddr_in 命名,定义在 <netinet/in.h>头文件中,形式如下:
- struct sockaddr_in
- sa_family_t sin_family; /* address family: AF_INET */
- in_port_t sin_port; /* port in network byte order */
- struct in_addr sin_addr; /* internet address */
- ;
sin_famliy 为套接字结构协议族,如IPv4为AF_INET;
sin_port 是16位 TCP或UDP端口号,网络字节顺序;
结构体成员in_addr也是一个结构体,定义如下:
- struct in_addr
- {
- uint32_t s_addr; /* address in network byte order */
- };
字节排序函数上面已经介绍,下面看一个实例:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #define PORT 2345
- int main()
- {
- int sockfd;
- struct sockaddr_in addr;
- int addr_len = sizeof(struct sockaddr_in);
- if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
- {
- perror("socket fail");
- exit(-1);
- }
- else
- {
- printf("socket created successfully!\nsocket id is %d\n",sockfd);
- }
- memset(&addr,0,addr_len);
- addr.sin_family = AF_INET;
- addr.sin_port = htons(PORT);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- if(bind(sockfd,(struct sockaddr *)(&addr),addr_len) < 0)
- {
- perror("bind error");
- exit(-1);
- }
- else
- {
- printf("bind port successfully!\nlocal port:%d\n",PORT);
- }
- return 0;
- }
- fs@ubuntu:~/qiang/net$ ./socket
- socket created successfully!
- socket id is 3
- bind port successfully!
- local port:2345
- fs@ubuntu:~/qiang/net$
3)等待监听函数
所谓监听,指的是socket 的端口一直处于等待的状态,监听网络中的所有客户机,耐心等待某一客户机发送请求。如果客户端有连接请求,端口就会接受这个连接。listen 函数用于实现服务器的监听等待功能,它的函数原型如下:
所需头文件 | #include <sys/socket.h> |
函数原型 | int listen(int sockfd, int backlog); |
函数传入值 | sockfd:套接字描述符 backlog:请求队列中允许的最大请求数,大多数系统默认值为5 |
函数返回值 | 成功:0 出错:-1 |
需要注意的是listen 并未真正的接受连接,只是设置socket 的状态为监听模式,真正接受客户端连接的是accept 函数 。通常情况下,listen 函数会在 socket ,bind 函数之后调用,然后才会调用 accept 函数。
listen函数只适用于SOCK_STREAM或SOCK_SEQPACKET 的socket 类型。如果socket 为 AF_INET ,则参数 backlog 最大值可设至128,即最多可以同时接受128个客户端的请求。
4)接受连接函数
服务器处于监听状态时,如果模式可获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时,再处理客户机的连接请求,接受连接请求的函数时accept,函数原型如下:
所需头文件 | #include <sys/socket.h> |
函数原型 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
函数传入值 | sockfd:套接字描述符 my_addr:用于保存客户端的地址,入参 addrlen:地址长度 |
函数返回值 | 成功:建立好连接的套接字描述符 出错:-1 |
当 accept 函数接受一个连接时,会返回一个新的 socket 标识符,以后的数据传输与读取就是通过这个新的socket 编号来处理,原来参数中的 socket 也可以继续使用。接受连接以后,远程主机的地址和端口信息会保存在 addr 所指的结构体内。
下面是个实例,体验listen 、accept函数的使用:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <unistd.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define PORT 2345
- int main()
- {
- int sockfd,newsockfd;
- struct sockaddr_in addr,caddr;
- int addr_len = sizeof(struct sockaddr_in);
- int caddr_len = sizeof(struct sockaddr_in);
- if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
- {
- perror("socket error");
- exit(-1);
- }
- else
- {
- printf("socket successfully!\n");
- printf("socket id : %d\n",sockfd);
- }
- memset(&addr,0,addr_len);
- addr.sin_family = AF_INET;
- addr.sin_port = htons(PORT);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- if(bind(sockfd,(struct sockaddr *)&addr,addr_len) == -1)
- {
- perror("bind error");
- exit(-1);
- }
- else
- {
- printf("bind successfully!\n");
- printf("local port : %d\n",PORT);
- }
- if(listen(sockfd,5) == -1)
- {
- perror("listen error");
- exit(-1);
- }
- else
- {
- printf("listening...\n");
- }
- if((newsockfd = accept(sockfd,(struct sockaddr *)&caddr,&caddr_len)) == -1)
- {
- perror("accept error");
- exit(-1);
- }
- else
- {
- printf("accepted a new connection ..\n");
- printf("new socket id : %d\n",newsockfd);
- }
- return 0;
- }
执行程序,得到输出结果:
- fs@ubuntu:~/qiang/netprogram/tmp$ ./lisacc
- socket successfully!
- socket id : 3
- bind successfully!
- local port : 2345
- listening...
程序运行到这停止,并一直在这里等待,说明本机计算机的 2345号端口正处于监听的状态,等待本机上的连接服务请求。此时打开浏览器,在浏览器地址栏中输入下列形式的地址:
- http://192.168.3.51:2345/
这个地址是笔者个人IP地址,按"ENTER" 键,这样浏览器会请求连接本地计算机上的2345号端口。此时终端中显示如下结果:
- accepted a new connection ..
- new socket id : 4
表明程序已经接受了这个连接,并创建了一个新的套接口(ID为4),然后退出了程序。
5、请求连接函数
所谓请求连接,是指在客户机向服务器发送信息之前,需要先发送一个连接请求,请求与服务器建立TCP通信连接。connect 函数可以完成这项功能,函数原型如下:
所需头文件 | #include <sys/socket.h> |
函数原型 | int connect(int sockfd, struct sockaddr * serv_addr, int addrlen); |
函数传入值 | sockfd:套接字描述符 sock_addr:服务器端地址 addrlen:地址长度 |
函数返回值 | 成功:0 出错:-1 |
这里ser_addr 是一个结构体指针,指向一个sockaddr 结构体,这个结构体存储着远处服务器的IP与端口号信息。
6、数据读写函数
TCP/UDP读写函数总结,注意函数要成对使用
1)send函数
建立套接口并完成通信连接以后,可以把信息传送到远程主机上,这个过程就是信息的发送。而对于远程主机发送来的信息,本地主机需要进行接收处理。下面开始讲述这种面向连接的套接口信息发送与接收操作。
用connect 函数连接到远程计算机以后,可以用 send 函数将应答信息发送给请求服务的本地主机,通信时双向的,并且通信的双方是对等的。
send() 函数原型如下:
所需头文件 | #include <sys/socket.h> |
函数原型 | int send(int sockfd, const void*buf, int len, int flags); |
函数传入值 | sockfd:套接字描述符 buf:发送缓冲区的地址 入参 len:发送数据的长度 flags:一般为0 |
函数返回值 | 成功:实际发送的字节数 出错:-1 |
2)recv()函数
函数recv 可以接收远程主机发送来的数据,并将这些数据保存到一个数组中,函数原型如下:
所需头文件 | #include <sys/socket.h> |
函数原型 | int recv(int sockfd, const void*buf, int len, int flags); |
函数传入值 | sockfd:套接字描述符 buf:存放接收数据的缓冲区 出参 len:接收数据的长度 flags:一般为0 |
函数返回值 | 成功:实际接收的字节数 出错:-1 |