什么是网络编程
网络编程简单来说就是编程使得计算机之间交互数据。
硬件部分已经接入了互联网中,我们需要考虑的是软件部分。
软件部分有OS提供的套接字。
套接字
套接字用于网络数据传输,这个名字看上去很奇特。socket也是“插座”的意思,意味着可以进行连接,就像用电设备连入电网一样。
套接字的构建
首先以TCP套接字为例,我们把它比为电话机
1.调用socket
首先需要安装电话机,对socket进行创建
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
2.调用bind函数
有了电话机,我们需要分配电话号码,也就是给socket分配IP地址和端口号。
#include <sys/socket.h>
int bind(int sockfd,struct sockaddr *myaddr,socklen_t addrlen);
3.调用listen函数
下一步需要连接电话线,转为接听状态
#include <sys/socket.h>
int listen(int sockfd,int backlog);
4.调用accept函数
电话来了当然是要接听的,这样才能接受对方的连接请求
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
以上就是套接字编程的基本流程了。
服务端和客户端
如何区分服务端还是客户端?
简单来说,服务端就是在后方默默服务的部分,主要用于接收请求,而客户端是要和用户进行交互的部分,主要是发送请求。
客户端在请求连接的时候,也会创建客户端套接字。
#include <sys/socket.h>
int connect(int sockfd,struct sockaddr *servaddr,socklen_t addrlen);
客户端只需要:
1.调用socket函数
2.调用connect函数发送
套接字协议
前面说完了创建sokcet,接下来细说其参数
int socket(int domain,int type,int protocol);
1.domain:套接字使用的协议族
2.type:套接字数据传输类型
3.protocol:计算机之间通信的协议
协议族
对于socket的协议族来说,PF_INET代表IPV4协议族,PF_INET6代表IPV6协议族。
第三个参数才是socket实际使用的协议。
套接字类型
1.面向连接的套接字
SOCK_STREAM,创建面向连接的套接字。(TCP)
数据会按顺序传递,并且不会消失。
并且有buffer,可以保存数据,在读取时可以一次性全部读出,也可以分几次读取。
如果明白TCP的原理,那么可以知道在缓冲区满时会停止传输,并且有重传机制。
2.面向消息的套接字
SOCK_DGRAM,创建面向消息的套接字(UDP)
速度更重要, 不按顺序,并且有可能会丢失数据。
而且数据传输有大小限制,接收数据次数与传输次数也相同。
基于UDP,所以遵循UDP的原理
协议选择
第三个参数负责选择协议。当协议族和类型确定之后,大部分情况都不需要第三个参数可为0。
除了一种情况,同一个协议族(比如IPV4)有多个数据传输方式相同的协议。
对于PF_INET+SOCK_STREAM的组合来说,只有IPPROTO_TCP一个协议。
对于PF_INET+SOCK_DGRAM的组合来说,只有IPPROTO_UDP一个协议。
地址族与数据序列
创建socket的下一步是调用bind函数,来分配IP地址和端口号。
IP地址分类IPV4和IPV6,IPV4又根据网络段和主机段分为ABCDE五类。(详解见计算机网络部分)
根据IP地址可以找到目标主机,而想要传输到对应的应用程序,就需要端口号了。
其中0-1023是用于分配给特定应用的知名端口。0-65535都是端口号的范围(2的16次方)
TCP套接字和UDP套接字的端口号可以重复,因为不会共用。
地址信息
在得到地址信息时,需要对三个问题进行回答。
1.采用什么地址族?IPV4
2.IP地址是多少?192.168.0.11
3.端口号是多少?2048
由此可以得到sockaddr_in结构体
struct sockaddr_in
{
sa_family_t sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
}
其中in_addr定义如下:
struct in_addr
{
in_addr_t s_addr;
}
数据类型
sa_family_t代表地址族。
unint16_t代表unsigned 16bit int。
in_addr_t 表示IP地址,声明为uint32_t;
结构体成员
sin_family用来保存地址族。比如AF_INET代表IPV4地址族,AF_INET6代表IPV6地址族。
sin_port保存16位端口号。
sin_addr保存32位IP地址,可以看该结构体的定义。(结构体的形式可能是为了可扩展性)
回到bind函数
bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
这里bind函数的第二个参数希望得到sockaddr的地址族,端口号,IP地址等信息。
而bind函数要求sockaddr结构体这样是这样的:
struct sockaddr
{
sa_family_ sin_family;
char sa_data[14];
}
这里sa_data保存地址信息中需要的IP和端口号。但比较麻烦,于是设计出子结构体,也就是我们上面看到的sockaddr_in。最后将其转换位sockaddr型的结构体变量。
这里的serv_addr就是sockaddr_in类型的。
struct sockaddr_in serv_addr;
所以我们上面看到的不使用的sin_zero[8]字段其实是为了进行填充,让其与sockaddr保持一致。
网络字节序
前面的16位端口号和32位IP地址,都是以网络字节序存储的。
我们都知道CPU向内存保存数据时有两种方式:大端序和小端序。
比如0x12345678,最高位是0x12,最低位是0x78.
在大端序中,最高位存放在低位地址。而小端序则相反,最低位存放在低位地址。
正是因为数据保存的顺序不同,所以在网络传输时需要约定好统一的方式,叫做网络字节序。
网络字节序统一为大端序。
而我们常用的intelCPU则是使用小端序,所以在接收数据时需要转换。(当然实际使用中无需手动转换)
字节序转换
字节序转换主要用到四个函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
根据名字也非常好理解。
h是host主机,n是network网络。
htons就是把short类型的数据从主机字节序转化为网络字节序。
ntohs就是把short类型的数据从网络字节序转化为主机字节序。
short用于端口号转换,long用于IP地址转换。
网络地址转换
inet_addr
前面说到,sockaddr_in保存地址信息是32位的,而我们日常使用的IP地址一般都是字符串信息。比如192.168.1.1
所以需要进行IP地址的转换
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);
这个函数可以将字符串形式的IP地址转换为32位整形数据,同时转换网络字节序。
char *addr1="1.2.3.256";
unsigned long conv_addr=inet_addr(addr1);
输出:
0x04030201
inet_aton
inet_addr函数的升级版就是这个inet_aton,该函数直接利用了in_addr结构体。
#include <arpa/inet.h>
int inet_aton(const char* string,struct in_addr *addr)
这样就不需要在转换IP地址后代入到sockaddr_in的in_addr结构体中。
char *addr="1.2.3.256";
struct sockaddr_in addr_inet;
if(!inet_aton(addr,&addr_inet.sin_addr))
error_handling("conversion error");
else
printf("Network ordered integer addr:%#x \n",addr_inet.sin_addr.s_addr)
输出:
Network ordered integer addr:0x04030201
inet_ntoa
也有一个函数用来将IP地址转换为字符串形式。
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);
需要注意的是返回值为字符串。所以调用后需要保存字符串,否则二次调用就会覆盖掉。
struct sockaddr_in addr1;
chr *str_ptr;
char str_arr[20];
addr1.sin_addr.s_addr=htonl(0x1020304);
str_ptr=inet_toa(addr1.sin_addr);
strcpy(str_arr,str_ptr);;
printf(str_ptr);
输出:
1.2.3.4
服务端初始化
根据上面了解的知识,我们可以写出服务端初始化的过程:
int serv_sock;
struct sockaddr_in serv_addr;
char* serv_port="9190";
//创建套接字
serv_sock=socket(PF_INET,SOCK_STREAM,0);
//初始化信息
memset(&serv_adr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(serv_port));
//分配地址信息
bind(serv_sock,(struct sockaddr*)&serv_addr),sizeof(serv_addr));
还有几个需要注意的点:
- memset函数将每个字节初始化为同一个值,这里就是0
- INADDR_ANY可以免去每次输入IP地址的步骤,自动获取计算机IP地址。
- atoi是把对应的字符串转换为数字
这样,我们就了解了网络编程的基本内容,在搭建基础的服务端客户端时这些知识会成为骨架。
|