signed

QiShunwang

“诚信为本、客户至上”

TCPsocket1

2021/5/14 21:05:51   来源:

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.c>
#include<netinet/in.c>
//以上两个头文件皆定义了这个结构体,考虑到后面还有一些转换用的函数,这两个头文件都建议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类型),以17为例子。对应的二进制码为00010001。如果计算机A从“左侧”开始发送,但是计算机B确按照顺序把这些位从右往左排列,那么结果就是10001000,既原数据的反码。

​ 为此,我们有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);