网络编程—套接字基础 & 基本TCP套接字编程-基本套接字函数

套接字基础

一个通用套接字地址结构sockaddr:

struct sockaddr
{
    unsigned short sa_family; //套接字的协议簇地址类型,AF_XX
    char        sa_data[14];//存储具体的协议地址
};

填充特定协议地址时使用sockaddr_in

struct sockaddr_in
{
    unsigned short  sin_len;         //IPv4地址长度
    short int   sin_family;   //指代协议簇,TCP套接字编程为AF_INET
    unsigned short  sin_port;     //存储端口号(使用网络字节顺序),数据类型是一个16位的无符号整数
    struct in_addr  sin_addr;   //存储IP地址,是一个in_addr结构体
    unsigned char   sin_zero[8];     //为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};

作为bind()、connect()、sendto()、recvfrom()等函数的参数时需要使用sockaddr,这时要通过指针强制转换的方式转为struct sockaddr*指针。

IPv4地址结构示例

struct sockaddr_in mysock;
mysock.sin_family = AF_INET;  //TCP地址结构
mysock.sin_port = htons(3333); //字节顺序转换函数
mysock.sin_addr.s_addr = inet_addr("166.111.160.10"); //设置IP地址
//如果mysock.sin_addr.s_addr = INADDR_ANY,则不指定IP地址(用于server程序)
bzero(&(mysock.sin_zero),8); //设置sin_zero为8位保留字节

IPV6套接字地址结构sockaddr_in6

#DEFINE SIN6_LEN
struct sockaddr_in6
{
    unsigned short  sin6_len;        //IPv6地址长度,是一个无符号的8位整数,表示128位的IPv6地址
    short int   sin6_family;   //地址类型为AF_INET6
    unsigned short  sin6_port;     //存储端口号,使用网络字节顺序
    unsigned short int sin6_flowinfo;  //低24位是流量标号,然后是4位优先级标志,剩下4位保留
    struct in6_addr sin6_addr;   //IPv6地址,网络字节顺序
};

struct in6_addr
{
    unsigned long   s6_addr;  //网络字节顺序的IPv6地址
};

IP地址转换函数

  • Inet_aton():将字符串形式的IP地址转换成二进制形式的IP地址,成功返回1,否则返回0,转换后的IP地址存储在参数inp中。
  • inet_ntoa():将32位二进制形式的IP地址转换为数字点形式的IP地址,结果在函数返回值中返回。

下面四个函数分别用于长整型和短整型数在网络字节序和主机字节序之间进行转换,其中s指short,l指long,h指host,n指network。

#include <netinet/in.h>
unsigned long htonl(unsigned long host_long);
unsigned short htons(unsigned short host_short);
unsigned long ntohl(unsigned long net_long);
unsigned short ntohs(unsigned short net_short);

套接字的工作原理
BSD套接字——INET套接字——TCP协议
进程在利用套接字进行通讯时,采用客户-服务器模型。服务器首先创建一个套接字,并将某个名称绑定到该套接字上,套接字的名称依赖于套接字的底层地址族,但通常是服务器的本地地址。
对于INET套接字来说,服务器的地址由两部分组成:服务器的IP地址和服务器的端口地址。已注册的标准端口可查看/etc/services 文件。
将地址绑定到套接字之后,服务器就可以监听请求连接该绑定地址的传入连接。
连接请求由客户生成,它首先建立一个套接字,并指定服务器的目标地址以请求建立连接。
传入的连接请求通过不同的协议层到达服务器的监听套接字。
服务器接收到传入请求后,如果能够接受该请求,服务器必须创建一个新的套接字来接受该请求并建立通信连接(用于监听的套接字不能用来建立通信连接),这时,服务器和客户就可以利用建立好的通信连接传输数据。

  1. 建立套接字——2. 在INET BSD套接字上绑定(bind)地址——3. 在INET BSD套接字上建立连接 (connect)——4. 监听(listen) INET BSD 套接字——5. 接受连接请求(accept)
    接受连接请求(accept):
    接受操作在监听套接字上进行,从监听 socket 中克隆一个新的 socket 数据结构。其过程如下:
    接受操作首先传递到支持协议层,即INET中,以便接受任何传入的连接请求。接受操作可以是阻塞的或是非阻塞的。非阻塞时,若没有可接受的传入连接,则接受操作将失败,而新建立的socket数据结构被抛弃。阻塞时,执行阻塞操作的网络应用程序将添加到等待队列中并保持挂起直到接收到一个TCP连接请求为止。
    当连接请求到达之后,包含连接请求的sk_buff被丢弃,而由TCP建立的新sock数据结构返回到INET套接字层,在这里,sock数据结构和先前建立的新socket数据结构建立链接。而新socket的文件描述符(fd)被返回到网络应用程序,此后,应用程序就可以利用该文件描述符在新建立的INET BSD套接字上进行套接字操作。

套接字为用户提供的系统调用
系统调用 说明
Accept 接收套接字上连接请求
Bind 在套接字绑定地址信息
Connet 连接两个套接字
Getpeername 获取已连接端套接字的地址
Getsockname 获取套接字的地址
Getsockopt 获取套接字上的设置选项
Listen 监听套接字连接
Recv 从已连接套接字上接收消息
Recvfrom 从套接字上接收消息
Send 向已连接的套接字发送消息
Sendto 向套接字发送消息
Setdomainname 设置系统的域名
Sethostid 设置唯一的主机标识符
Sethostname 设置系统的主机名称
Setsockopt 修改套接字选项
Shutdown 关闭套接字
Socket 建立套接字通讯的端点
Socketcall 套接字调用多路复用转换器
Socketpair 建立两个连接套接字

gethostbyname():主机名转换为IP地址
gethostbyaddr():IP地址转换成主机名
getservbyname():根据给定名字查找相应服务,返回服务的端口号
getservbyport():给定端口号和可选协议查找相应服务
gethostbyname():主机名转换为IP地址
gethostbyaddr():IP地址转换成主机名
getservbyname():根据给定名字查找相应服务,返回服务的端口号
getservbyport():给定端口号和可选协议查找相应服务
字节处理函数
套接字地址是多字节数据,不是以空字符结尾的,Linux提供两组函数来处理多字节数据。一组函数以b开头,适合BSD系统兼容的函数;另一组函数以mem开头,是ANSI C提供的函数。

#include<string.h>
void bzero(void *s,int n);
函数bzero将参数s指定的内容的前n个字节设置为0,通常用它来将套接字地址清零。
void bcopy(const void *src,void *dest,int n);
函数bcopy从参数src指定的内存区域拷贝指定数目的字节内容到参数dest指定的内存区域
void bcmp(const void *s1,const void *s2,int n);
函数bcmp比较参数s1指定的内存区域和参数s2指定的内存区域的前n个字节内容,相同则返回0,否则返回非0
void *memset(void *s,int c,size_t n);
将参数s指定的内存区域的前n个字节设置为参数c的内容
void *memcpy(void *dest,void *src,size_t n);
类似于bcopy,但bcopy能处理参数src和参数dest所指定的区域有重叠的情况,而memcpy不能
void memcmp(const void *s1, const void *s2,size_t n);
比较参数s1和参数s2指定区域的前n个字节内容,相同则返回0,否则返回非0

基本TCP套接字编程-基本套接字函数

1.创建套接字

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,  int type,  int protocol);
返回:若成功返回一个正整数(套接字描述符),否则返回-1

套接字的域名(domain),代表套接字协议族
套接字的类型(types),最常用的值是SOCK_STREAM、SOCK_DGRAM和SOCK_RAW
使用的协议(protocol),一般情况下该参数为0,表示由系统在当前设定的domain下,自动选择适合的协议类型

sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd<0){
    fprintf(stderr,”socket error:%s\n”,strerror(errno);
    exit(1);
}

Linux系统中创建一个套接字的操作主要是:
在内核中创建一个套接字数据结构,然后返回一个套接字描述符。这个套接字数据结构包含连接的各种信息。例如:对方地址,TCP状态,以及发送和接收缓存等。
TCP协议根据这个套接字数据结构来控制这条连接。

实际上,套接字对于用户程序而言就是特殊的已打开的文件。内核中为套接字定义了一种特殊的文件类型,形成一种特殊的文件系统sockfs。
所谓创建一个套接字,就是在sockfs文件系统中创建一个特殊文件,或者说一个节点,并建立起为实现套接字功能所需的一整套数据结构。
所以,函数sock_create()首先是建立一个socket数据结构,然后将其“映射”到一个已打开的文件中,进行socket结构和sock结构的分配和初始化。

  1. 流式套接字连接发起
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, struct sockaddr_in *serveraddr, int serveraddrlen);
返回:成功返回0,失败返回-1,并设置errno为以下任一种错误。

ETIMEDOUT:若客户端TCP在发出首个SYN分段后没有收到任何应答,则大约在调用connect()函数75秒后将返回该错误;
ECONNREFUSED:当服务器进程并未启动,此时客户端TCP将向客户端应用返回一个RST错误 。
EHOSTUNREACH或ENETUNREACH:若客户端TCP发出的SYN分段收到了途经的中间路由器的“目标不可达”ICMP报文,则客户端TCP会重发SYN分段直到超过75秒,此时客户端TCP向客户端应用返回该错误
按照TCP状态转换图,connect函数导致当前套接字从CLOSED状态转移到SYN_SENT状态。
若成功则再转移到ESTABLISHED状态。
若失败则该套接字不再可用,必须关闭,不能对这样的套接字再调用connect。
在调用connect前,客户机需要指定服务器进程的套接字地址。
客户机一般不指定自己的套接字地址,系统会自动从1024至5000的端口范围内为它选择一个未用的端口号,然后以这个端口号和本机的IP地址填充套接字地址。

建立一个TCP连接的操作一般如下:
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
if(inet_aton(argv[1], &servaddr.sin_addr)<0){
fprintf(stderr,“inet_aton error\n”;
exit(1);
}
if(connect(sockfd, (struct sockaddr *) &servaddr,sizeof(servaddr))<0){
fprintf(stderr,“connect error:%s\n”,strerror(errno));
exit(1);
}


  1. 绑定套接字

  • int bind (int sockfd, struct sockaddr_in *localaddr, int localaddrlen);
    返回:成功返回0,否则返回-1,并设置errno,错误类型为EADDRINUSR。
    服务器和客户机都可以调用函数bind来绑定套接字地址,但是一般都是服务器调用bind来绑定自己的公认端口号。绑定操作一般如下:

bzero(&my_addr,sizeof(my_addr));
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(PORT);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { 
    fprintf(stderr,“bind to port %d error!\n”,PORT);
    exit(1); 
}


  1. 流式套接字连接监听

  • int listen (int sockfd, int backlog ); 返回:成功返回0,否则返回-1。
    socket创建的主动套接字,可以用它来进行主动连接(调用connect),但是不能接收连接请求。而服务器的套接字必须能都接收客户机的请求。listen将一个尚未连接的主动套接字转换成一个被动套接字:
    告诉TCP协议,这个套接字可以接收连接请求;
    执行listen后,TCP状态由CLOSED状态转换成LISTEN状态。
    TCP将到达的连接请求排队,backlog指定这个队列的最大长度。
  • 流式套接字连接接收
  • int accept (int sockfd, struct sockaddr_in clientaddr, int clientaddrlen);
    返回:成功返回连接套接字描述符>0,否则返回-1。
    accept从监听套接字的完成连接队列中接收一个已经建立的TCP连接。TCP协议创建一个新的连接套接字来标识这个要接收的连接,并将其描述符返回给应用程序。
    监听套接字
    连接套接字
  • 关闭套接字描述符

#include <unistd.h>
int close(int sockfd);
int shutdown (int sockfd, int how);

close将这个套接字描述符标记为关闭状态(不同于CLOSED),然后立即返回进程。
管理套接字后后进程将不能访问这个套接字。但是TCP协议继续使用这个套接字,将尚未发送的数据传递到对方,然后发送FIN数据段,执行关闭操作,一直等到TCP连接完全关闭后,TCP才删除这个套接字。
7. 读流式套接字

#include <unistd.h>
int read(int sockfd,  const void *buf,  int buflen);
返回:实际所读取的数据字节长度,0表示读到文件尾,-1表示错误

每个套接字都有两个缓冲区:接收缓冲区和发送缓冲区
如果可读数据大于len指定值,则返回指定长度的值
如果可读数据量小于len,read不等待所有数据都到达,而是立即返回缓冲区所有数据,此时返回值小于len。①
当无数据可读时,read将阻塞,等待数据到达。
当程序从一个套接字读数据时,有以下几种情况:
套接字接受缓冲区中接收到数据(返回>0)
接收到FIN数据段(返回=0)
接收到RST数据段(返回=-1,ECONNRESET)(当TCP协议接收到RST数据段,表示连接出现了某种错误,函数read将以错误返回,错误类型为ECONNERESET。)
进程阻塞过程中接收到信号(返回=-1,EINTR)②
在上面所述的两种情况下并不表示读操作发生错误。所以用下面的readn函数继续读操作。
8. 写流式套接字

#include <unistd.h>
int write(int sockfd,  const void *buf,  int buflen);
返回:实际所写的数据字节长度,-1表示错误

当程序从一个套接字写数据时,有以下几种情况:
发送缓冲区有足够的空间(返回>0)
接收到RST数据段(返回=-1,EPIPE)
进程阻塞过程中接收到信号(返回-1,EINTR)
为了处理EINTR错误,继续写操作,通常用下面的writen实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值