函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//成功时返回文件描述符,失败时返回-1。
domain //套接字中使用的协议族(Protocol Fam ily ) 信息。
type //套接字数据传输类型信息。
protocol //计算机间通信中使用的协议信息。
协议族( Protocol Family)——————————————
PF_INET //IPv4互联网协议族
PF_INET6 //IPv6互联网协议族
PF_LOCAL //本地通信的UNIX协议族
PF_pACKET //底层套接字的协议族
PF_IPX //IPX Novell 协议族
另外,套接字中实际采用的最终协议信息是通过socket函数的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数。
套接字类型( Type )——————————————
套接字类型指的是套接字的数据传输方式。
套接字类型1 : 面向连接的套接字(SOCK_STREAM)
可靠的、按序传递的、基于宇节的面向连接的数据传输方式的套接字。
传输方式特征:
- 传输过程中数据不会消失。
- 按序传输数据。
- 传输的数据不存在数据边界(Boundary)。
套接字类型2: 面向消息的套接字(SOCK_DGRAM)
不可靠的、不按序传递的、以数据的高速传输为目的的套接字。
传输方式特征:
- 强调快速传输而非传输顺序。
- 传输的数据可能丢失也可能损毁。
- 传输的数据有数据边界。
- 限制每次传输的数据大小。
协议类型( protocol)——————————————
该参数决定最终采用的协议。
IPPROTO_TCP | IPPTOTO_UDP | IPPROTO_SCTP | IPPROTO_TIPCTCP |
---|---|---|---|
TCP传输协议 | UDP传输协议 | STCP传输协议 | TIPC传输协议 |
IPv4协议族中面向连接的套接字
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
IPv4协议族中面向消息的套接字
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
TCP套接字和UDP套接字不会共用端口号,所以允许重复。
怎么判断一个文件描述符是不是套接字描述符?
int issockettype(int fd)
{
struct stat st;
int err = fstat (fd, &st);
if(err < 0){
return -1;
}
if ((st.st_mode & S_IFMT) == S_IFSOCK){
return 1;
} else {
return 0;
}
}
1 地址族与数据序列
scoker结构体定义:
struct sockaddr_in
{
sa_family_t sin_family;//地址族( Address Family ) 4字节
uint16_t sin_port;//16 位TCP/UDP 端口号 4字节
struct in_addr sin_addr;//32 位IP 4字节
char sin_zero[8);//不使用
};
成员sin_family
每种协议族适用的地址族均不同。
AF_LOCAL只是为了说明具有多种地址族而添加的。
成员sin_port
该成员保存16位端口号,重点在于它以网络字节序保存(关于这一点稍后将给出详细说明)。
成员sin_addr
该成员保存32位IP地址信息,且也以网络字节序保存。为理解好该成员,应同时观察结构体in_addr 。但结构体in_addr声明为uint32_t, 因此只需当作32位整数型即可。
成员sin_zero
无特殊含义。只是为使结构体sockaddr_ in的大小与sockaddr结构体保持一致而插入的成员。必需填充为0 , 否则无法得到想要的结果。后面会另外讲解sockaddr。
但是绑定函数没有直接使用该结构体,而是用了地址。
SOCKADDR_IN servAddr;
//...
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)//第二步
ErrorHandling("bind() error");
//...
因为上面的第二个参数的结构如下,如果直接进行设置IP地址和端口号,不方便,所以就用了C语言的一个小技巧。
typedef struct sockaddr {
ADDRESS_FAMILY sa_family; // Address family. 4字节
CHAR sa_data[14]; // Up to 14 bytes of direct address.
} SOCKADDR;
typedef USHORT ADDRESS_FAMILY;
typedef unsigned short USHORT;
sockaddr_in是sockaddr继承而来。
sockaddr _in是保存IPv4地址信息的结构体。那为何还需要通过sin_family单独指定地址族信息呢?这与之前讲过的sockaddr结构体有关。结构体sockaddr并非只为IPv4设计,这从保存地址信息的数组sa_data长度为14 宇节也可看出。因此,结构体sockaddr要求在sin_family 中指定地址族信息。为了与sockaddr保持一致, sockaddr_ in结构体中也有地址族信息。
2 字节序(Order) 与网络字节序
CPU 向内存保存数据的方式有2种,这意味着CPU解析数据的方式也分为2种。
- 大端序(Big Endian ): 高位字节存放到低位地址。
- 小端序(Little Endian ): 高位字节存放到高位地址。
如果发送短用大端,接收短用小端,则会出问题,因此,在通过网络传输数据时约定统一方式,这种约定称为网络字节序(Network Byte Order), 非常简单一~统一为大端序。
即,先把数据数组转化成大端序格式再进行网络传输。因此,所有计算机接收的数据是网络字节序格式,小端序系统传输数据时应转化为大端序排列方式。
字节序转换( Endian Conversions)
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
通过函数名应该能掌握其功能,只需了解以下细节。
- htons中的h代表主机(host) 字节序。
- htons 中的n代表网络(network) 字节序。
另外, s指的是short, l指的是long ( Linux 中Ion砓皂型占用4个字节,这很关键)。因此, htons是h 、to 、n 、s的组合,也可以解释为“把short型数据从主机宇节序转化为网络宇节序”。
如:ntobs可以解释为“把short型数据从网络宇节序转化为主机宇节序” 。
接收数据不用考虑字节序的问题。
则,除了向sockaddr_ in结构体变量填充数据外,其他情况无需考虑字节序问题。
字符串形式的IP地址转换成32位整数型数据:
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);
另一个函数利用了in_addr结构体,且其使用频率更高。
#include <arpa/inet.h>
int inet_aton(const char* string, struct in_addr * addr);
可以这样使用:
inet_aton(addr, &addr_inet.sin_addr);
字节序到字符串:
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);
网络地址初始化步骤:
struct sockaddr_in addr;
char* serv_ip = "211.117.168.13";//声明IP 地址字符串
char * serv_port = "9190";//声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量 addr 的所有成员初始化为 0
addr.sin_family = AF_INET;//指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip);//基于宇符串的IP 地址初始化
addr.sin_port = htons(atoi(serv_port));//基于宇符串的端口号初始化
INADDR_ANY
每次创建服务器端套接字都要输入IP地址会有些繁琐, 此时可如下初始化地址信息。
struct sockaddr_in addr;
char * serv_port = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
与之前方式最大的区别在于,利用常数INADDR_ANY分配服务器端的IP地址。若采用这种方式, 则可自动获取运行服务器端的计算机IP地址, 不必亲自输入。而且,若同一计算机中已分配多个IP地址(多宿主( Multi-homed ) 计算机, 一般路由器属于这一类), 则只要端口号一致,就可以从不同IP地址接收数据。因此,服务器端中优先考虑这种方式。而客户端中除非带有一部分服务器端功能, 否则不会采用。