#include
int socket(int domin,int type,int protocol);
面向连接的套接字SOCK_STREAM | 面向消息的套接字SOCK_DGRAM |
---|---|
传输过程中数据不会消失 | 传输的数据可能丢失可能损毁 |
按序传输数据 | 强调快速传输而非传输顺序 |
传输的数据不存在边界 | 传输的数据存在边界 |
限制每次传输的数据大小 |
eg
IPv4协议族中面向连接的套接字
int tcp_socket = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
IPv4协议族中面向消息的套接字
int udp_socket = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);
①IP地址
IP是网络协议的简写,是为了收发网络数据而分配给计算机的值,IP地址分为两类IPv4和IPv6,IPv4是四字节地址族、而IPv6是16字节地址族。
②端口号
端口号是为了区分程序中创建的套接字而分配给套接字的序号,也就是说操作系统此端口号把数据传输给相应端口的套接字。
端口号由16位构成,可分配的范围0-65535(0 - 2^16 -1)但是0-1023分配给特定程序,所以应当分配此范围外的值。注:TCP套接字和UDP套接字不会共用端口号,允许重复。
本节以IPv4为核心,讨论目标地址的表示方法,假如我们需要表示IPv4地址族,IP地址是211.204.214.76,端口号2048,我们需要用一个结构体作为地址传递给bind函数
struct sockaddr_in
{
sa_family_t sin_family;//地址族
unint16_t sin_port;//16位端口号unsigned short类型
struct in_addr sin_addr;//32位IP地址
char sin_zero[8];//不使用
}
其中内含的另一个结构体in_addr用来存放32位IP地址如下
struct in_addr
{
in_addr_t s_addr;//32位IPv4,unsigned long类型
}
下面说明sockaddr_in各个成员
int bind(int sockfd,struct sockaddr,socklen_t addrlen);
由于bind函数需要struct sockaddr
的参数,如下struct sockaddr
{
sa_family_t sin_family;
char sa_data[14];//地址信息
}
而我们使用的是struct sockaddr_in
所以我们需要进行转换**(struct sockaddr*)&serv_addr**
eg
struct sockaddr_in serv_addr;
...
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1)
//do something
...
可能有些朋友看到这会有些迷惑,这么多结构体是干什么的,其实我们只要明白,bind函数第二参数需要的struct sockaddr
,而这个结构体不好用,因为他的IP地址、端口号、填充的0都在一个变量内,所以我们使用好用的struct sockaddr_in
分别记录上述的东西,最后通过转换变成bind函数的第二参数。
在(2)中sockaddr_in的成员sin_port、sin_addr需要以网络字节序保存端口号与IP地址信息,那么为什么强调网络字节序呢?
其实CPU有两种保存数据的方式,即大端序与小端序,保存顺序的不同意味着接受数据的解析顺序不同,所以需要统一
大端序:高位存放到地位地址
小端序:高位存放到高位地址
而网络字节序使用大端序格式进行网络传输,所以在填充sockaddr_in
之前小端序统统转化为大端序排列。下面介绍四个函数把主机字节序转化为网络字节序
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
htons中h代表主机字节序(host),n代表网络字节序(net),而末尾的s、l代表short和long,这样就好理解了。把short类型数据从主机字节序转化为网络字节序。
short用于转换端口号、long用于转换IP地址
现在我们可以进行网络地址的初始化也就是对上面的结构体进行赋初值
struct sockaddr_in addr;
char* serv_port = "9190"; //声明端口号字符串
memset(&addr,0,sizeof(addr));//addr所有成员初始化为同一值(sin_zero)
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = htonl(INADDR_ANY);//自动获取服务端ip地址
addr.sin_port = htons(atoi(serv_port)); //端口号初始化,atoi是把字符串转换成整型数的一个函数
此外还可以输入ip地址对addr.sin_addr.s_addr
初始化,这里就需要检测ip地址是否合法
①inet_addr函数
#include
in_addr_t inet_addr(const char* string);
eg
struct sockaddr_in addr;
char* serv_ip = "211.217.168.13";
char* serv_port = "9190";
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(serv_ip);
addr.sin_port = htons(atoi(serv_port));
②inet_aton函数
与inet_addr函数作用相同,只不过利用了struct in_addr结构体
#include
in_addr_t inet_aton(const char* string,struct in_addr *addr);
前面花了大篇幅说明了sockaddr_in初始化的方法,现在我们可以向套接字分配网络地址
#include
int bind(int sockfd,struct sockaddr* myaddr,socklen_t addrlen);
整个初始化过程:
int serv_sock;
struct sockaddr_in serv_addr;
char* serv_port = "9190"; //声明端口号字符串
//创建套接字
serv_sock = sock(PF_INET,SOCK_STREAM,0);
//地址信息初始化
memset(&addr,0,sizeof(addr));//addr所有成员初始化为同一值(sin_zero)
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = htonl(INADDR_ANY);//自动获取服务端ip地址
addr.sin_port = htons(atoi(serv_port)); //端口号初始化
//分配地址信息
bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr);
...
在已经用bind函数分配地址后,需要调用listen函数进入等待连接请求状态。只有调用了listen函数后,客户端才能调用connect函数
#include
int listen(int sock,int backlog);
eg
int serv_sock;
if(listen(serv_sock,5) == -1)
//do something
由于服务端套接字在listen函数中充当门卫,那么需要新建套接字连接到发起请求的客户端
#include
int accept(int sock,struct sockaddr *addr,socklen_t *addrlen);
eg
int clnt_sock;
socklen_t clnt_addr_size;
clnt_addr_size = sizeof(clnt_sock);
clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size));
上面分析了服务端的函数,剩余一个客户端的connect函数用于请求连接
#include
int connect(int sock,struct sockaddr *servaddr,socklen_t addlen);
eg
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1)
//do something
客户端调用connect函数后两种情况返回:
客户端在调用connect函数时使用本机IP,随机出端口进行自动分配,无需调用bind函数
好了,终于搞明白各函数的用法了,下一篇我们来实现一个简单的服务端与客户端