TCP Socket 通信的一些基本知识(UNIX环境)
表示符(XXfd)
在Unix系统中,任何计算机资源都可以以“文件”的形式来表示和管理,这就包括了我们下面要介绍的socket,以及stdin,stdout和stderr。它们均可以以一个整数型变量存储。
创建Socket
#include<sys/socket.h>
sockfd=socket(int family,int type,int protocol);
//创建不成功会返回-1
//such as
sockfd=socket(AF_INET,SOCK_STREAM,0);
其中,family指的是地址族,可以通俗地理解为IP协议类型,AF_INET 为IPv4,AF_INET6为IPv6。
type指的是socket类型,面向“流”还是“字节”。前者使用SOCK_STREAM后者使用SOCK_DGRAM.
protocol指的是socket协议,TCP or UDP,分别对应PPORTO_TCP和PROTO_UDP。以0为参数时,会使用相应type的默认协议(TCP,UDP)。
注:以上均为常量,使用时不需要打双引号
为服务器端监听Socket指定端口
和常见的面向对象编程不同,我们会将这些信息先封装到sockaddr或sockaddr_in结构体中,然后再通过bind函数来“绑定”。
sockaddr
#include<sys/socket.h>
struct sockaddr{
unint8_t sa_len;//由无符号八位整数存储的结构体size
sa_family_t sa_family;//地址族(见上),实际上就是整数储存
char sa_data[14];//表面上是一个字符数组,实际上包含了地址和端口信息
}
我们现在逐渐开始用sockaddr_in 来代替sockaddr
sockaddr_in
#include<arpa/inet.h>
#include<netinet/in.h>
//以上两个头文件皆定义了这个结构体,考虑到后面还有一些转换用的函数,这两个头文件都建议include
struct sockaddr_in{
uint8_t sin_len;//结构体size
sa_family_t sin_family;//地址族
in_port_t sin_port;//端口号
struct in_addr sin_addr;//地址,注意!这里设计到另外一个结构体
char sin_zero[];
}
struct in_addr{
unsigned long s_addr;
}
主机字节序和网络字节序的转化
我们都知道,计算机系统一般以字节为单位提供寻址,以位为单位发送。同时,我们所熟知的各个类型都以字节为存储单位。那么,当我们发送一个字节时需要注意什么呢?很明显,顺序。
例如,我使用一个字节来存储无符号整数(uint_8_t类型),以18为例子。对应的二进制码为0001 0010。如果计算机A从“左侧”开始发送,但是计算机B确按照顺序把这些位从右往左排列,那么结果就是0010 0001。
为此,我们有htons,ntohs;htonl,ntohl;这两组函数解决这个问题。
其中hton是“host to network”的缩写,ntoh是“networl to host”的缩写。
uint16_t htons(unit_16_t value)
uint32_t htonl(unit_32_t value)
uint16_t ntohs(unit_16_t value)
uint32_t ntohs(unit_32_t value)
在设定sockaddr_in 的端口时需要用到这些函数
struct sockaddr_in sockaddr;
sockaddr.sin_addr.s_addr=htonl(INADDR_ANY);//INADDR_ANY 指的是本机地址,server端用这个设定监听socket的地址
sockaddr.sin_port=htons(port);
为sockaddr_in 设定地址
我们看到,该数据类型存储地址的变量是另外一个结构体。为此,我们有办法直接将字符串表示的IPv4/IPv6地址直接写入 in_addr结构体中
#include<arpa/inet.h>
int inet_pton(int family,const char *strptr,void *addptr);//presentation to numeric, 成功则返回1,输入无效返回0,出错返回-1
char* inet_ntop(int family,const struct *addrptr,char *strptr,size_t len)//成功返回字节符指针,出错则返回NULL
//len 是指定的存储区的大小,如果太小会返回NULL以防止溢出
struct sockaddr_int sockaddr;
sockaddr.sin_port = htons(port);
sockaddr.sin_family = AF_INET;
inet_pton(AF_INET, destaddr, &sockaddr.sin_addr);
bind函数
既然我们已经设定好sockaddr_in结构体,下面让我们对其进行绑定
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,struct sockaddr* addr,socklen_t addrlen);//不成功会返回-1
//such as
bind(lisfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr);
//注意,因为我们使用了sockaddr_in,所以需要指针类型的强制类型转换
将服务器监听socket 转入监听状态,建立连接
listen函数
int listen(int sockfd,int num);//num是最大排队个数
//suchas
listen(lisfd,5)
accept函数
int accept(int sockfd,struct * sockaddr,sockelen_t * addrlen);
//注意,第二个参数用于返回对方服务器的协议地址,第三个参数是对应结构体的大小。这两个一般都是NULL
//建立连接后,服务器会建立一个新的socket,它的表示符就是这个函数的返回值
client连接服务器
connect 函数
int connect(int sockfd,const struct * sockaddr addr, socklen_t addrlen);
//第二个参数就是目标服务器的协议地址信息
//第三个参数是对应结构体的大小
//如果连接不成功会返回负数
//suchas
connect(sockfd, (struct sockaddr *) &sockaddr, sizeof(sockaddr));.
下面给出客户机器端一个socket从创建到建立连接的过程
int sockfd;
struct sockaddrin sockaddr;
sockaddr.sin_famimly=AF_INET;
sockaddr.sin_port=htons(port);
char * destaddr=(char *)"0.0.0.0";
inet_pton(AF_INET,destaddr,&(sockaddr.sin_addr));
connect(sockfd,(struct sockaddr*)&sockaddr,sizeof(sockaddr));
读(read)和写(write)
上面提到,socket也可以视作文件,他们之间信息的收发也可以理解为读操作和写操作。
注意,以下两个函数都是阻塞的,一旦调用,只有发送(并被接收)/读到数据才会结束。
### write
int write(int sockfd,char * buf,size_t len);
//第二个参数是要发送的数据的地址(通常是一个字符串数组)
//第三个参数是要发送的长度
//返回值是实际发送长度,负数为出错,0为连接断开
read
int read(int sockfd,char * buf,size_t len);
//第二个参数是要缓冲区地址(通常是一个字符串数组)
//第三个参数是要发送的长度
//返回值是实际接收长度,负数为出错,0为连接断开
断开连接
close
int close(int sockfd);