首先对于访问上篇博文(【网络基础-计算机字节序】)的同学们表示感谢,对于第一篇博文就有1k左右的浏览量“零号”还是很开心的。以后也会争取给大家发布更有价值的,通俗易懂的博文。“零号‘’是开心了,但是“壹号”同学就没有这么顺利,他又面试失利了,他上篇博文搞定了“网络套接字”的问题,但这次又败在了对TCP套接口编程的理解上,此篇博文我们就说一说TCP套接口编程到底有哪些难搞的地方,有哪些坑容易被问到。照例,先列出失败的问题:
面试官问题1:TCP客户端与服务器端的创建过程您了解吗?
面试官问题2:可以谈谈您对于每个函数的理解吗?
面试官问题3:可以说一说您对listen以及accept函数的理解吗?
首先,先来了解一下TCP套接口编程的过程。如图1(图片来自UNIX网络编程-卷一):
图1
从上图可以看出,客户与服务器的通信流程为:
服务器端:
- 调用socket创建套接字描述字
- 调用bind绑定服务ip和端口号
- 调用listen将主动连接套接字改为被动连接套接字
- 调用accept等待客户端连接
- 有客户连接后:
- read读取客户发送来的数据
- 处理用户请求
- write给客户发送回复信息
- 处理完成后,调用close关闭accept返回的已连接套接字
- 有客户连接后:
客户端:
- 调用socket创建套接字描述字
- 调用connect请求连接服务器
- 连接成功后:
- write发送请求
- read接收服务器的应答
- 进行业务处理
- 处理完成后,调用close关闭socket创建的套接字描述字
- 连接成功后:
服务器端在通信过程中执行的接口分别为,socket,bind,listen,accept,read,write,close;客户端在通信过程中分别调用的接口为socket,connect,write,read,close。
下面我们来详细说一说通信过程中用到的这些接口及其功能:
socket:创建套接口描述字(套接字)
头文件:#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回值:成功:非负描述字,失败:-1
参数说明:
family:协议簇选项,当前套接口的协议类型。图2解释了不同值代表的意义
type:常用类型,TCP一般为字节流套接口,UDP一般为数据报套接口,如下图3
protocol:协议号,通常设置为0,但在原始套接口上可能会有不同。
图2
图3
family&type组合成的协议类型如下图4,从图中我们可以看出TCP都是字节流型的报文,UDP都是数据报类型的报文:
图4
connect:客户端用来建立与服务器的连接
头文件:#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen);
返回值:成功:0 错误:-1
参数说明:
sockfd:套接口描述字,客户端socket函数创建的套接字
servaddr:套接口地址结构指针,其内部必须包含服务器的IP地址和端口号
addrlen:套接口地址结构大小
延伸:在TCP连接过程中,connect函数激发TCP的三路握手过程,且仅在连接建立成功或出错时才返回,返回的错误可能有以下几种情况:
- tcp客户没有收到SYN分节的响应,则返回ETIMEDOUT。
- 客户接受到RST响应,则表明该服务器主机在我们指定的端口上没有进程与之连接。
- 如果某客户发出的SYN在中间的路由器上引发了一个目的地不可达ICMP错误。客户保存此消息,并按一定的时间间隔连续发出SYN。若在规定时间后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSHTUNREACH或ENETUNREACH错误返回给进程。
注:如果connect函数返回失败,则套接口不可再用,必须关闭,不能再对此套接口再调用connect函数去连接服务器。
bind:将套接字绑定一个IP地址和端口号
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr * myaddr,socklen_t addrlen);
返回:成功:0 出错:-1
参数说明:
sockfd:套接口描述字,客户端socket函数创建的套接字
servaddr:套接口地址结构指针,内部设置的是将要给sockfd绑定的ip以及端口号
addrlen:套接口地址结构大小
延伸:对于TCP,调用bind可以指定一个端口号,指定一个IP地址,可以两者都指定,也可以一个也不指定。当服务器启动时,要通过bind捆绑一个端口,如果TCP客户或服务器不这么做,当调用函数connect或listen时,内核就要为套接口选则一个临时端口。大多数TCP客户都是这么做的。但是对于TCP服务来说是及其少见的,因为客户连接服务器需要知道服务器的端口。
进程可以把一个特定的IP地址捆绑到它的套接口上。客户和服务器绑定情况如下:
- 如果参数servaddr指定了ip和端口号,就是为参数sockfd这个套接字绑定ip和端口号。
- 客户端调用bind:客户在与服务器通信时,报文中源IP与源端口号就是客户端调用bind绑定的地址。
- 服务器调用bind:对于多ip服务器如果绑定的是INADDR_ANY那么对于服务器所有ip的访问都可以被支持,如果只绑定了服务器的IP A,则只有对A ip及绑定端口号的服务会被支持。
注:如果TCP服务器不把IP地址捆绑到套接口上,内核就把客户所发SYN所在分组的目的IP地址作为服务器的源IP地址。(效果跟绑定通配地址符相同)
对于服务器调用举个粟子:
一台服务器有两个ip,ip1:192.10.1.110、ip2:192.10.1.111,在创建socket服务时分为下列几种情况:
- bind绑定的是INADDR_ANY(通配地址符),那么客户连接该服务时,通过ip1或ip2都可以连接成功。
- bind绑定了其中一个ip(假设为ip1),那么客户端只能通过绑定ip(ip1)进行访问,所有通过其它ip(ip2)访问的将连接失败。
- 没有调用bind函数,会由内核选择端口号,可以通过getsockname函数返回。
bind函数设置ip和端口号的组合,导致的结果,如下图5:
图5
注:如果让内核来为套接口选择端口号,函数bind并不返回所选择的值。为了得到内核所选择的这个临时端口值,必须调用函数getsockname来返回协议地址(getsockname函数后面会介绍)。
常见错误:EADDRINUSE(地址已使用,当前绑定ip及port已经被占用且没有设置SO_REUSEADDR和SO_REUSEPORT属性)
listen:将主动套接口转换为被动套接口
#include<sys/socket.h>
int listen(int sockfd,int backlog);
返回值:成功:0 出错:-1
参数说明:
socket:套接字描述字
backlog:内核为此套接口排队的最大连接个数(已连接队列,未连接队列两个队列总和的最大值)。
说明:当函数socket创建一个套接口时,它被假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口,函数listen将未连接的套接口状态转换成被动套接口,指示内核应接受指向此套接口的连接请求。根据TCP套接口转换图,调用Listen导致套接口从CLOSED状态转换到LISTEN状态。
延伸:对于给定监听套接口,内核要维护两个队列:
- 未完成连接队列,为每个这样的SYN分节开设一个条目:已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手连接。这些套接口都处于SYN_RCVD状态。
- 已完成连接队列,为每个已完成TCP三路握手过程的客户开设一个条目。这些套接口都处于ESTABLISHED状态。
下图是这两个在TCP连接过程中的图示(图6:TCP监听套接口维护的两个队列;图7:TCP三路握手和监听套接口的两个队列交互过程):
图6
图7
注:当一个客户SYN到达时,若两个队列都是满的,TCP就忽略此分节,且不发送RST。因为这种情况是暂时的,客户TCP将重发SYN,期望不久就能在队列中找到空闲条目。要是TCP服务器发送了一个RST,客户的connect函数将立即返回一个错误,强制应用进程处理这种情况,而不是让TCP正常的重传机制来处理。
accept:接受客户端的连接,返回已连接套接字
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);
返回值:成功:非负描述字 出错:-1
参数说明:
sockfd:监听套接口描述字(一个给定的服务器常常是只生成一个监听套接口且一直存在,直到服务器关闭)
cliaddr:发起连接请求的客户端网络套接字结构地址,如果不在意可以设为NULL
addrlen:网络套接字地址结构长度
返回值说明:已连接套接口描述子(内核为每个被接受的客户连接创建了一个已连接套接口。当服务器完成某客户的服务时,关闭已连接套接口)。该函数最多返回三个值,一个即可能是新套接口描述字也可能是错误指示的整数,一个客户进程的协议地址(由指针cliaddr所指)以及该地址的大小(由指针addrlen所指)。
accept执行成功返回的是内核自动生成一个全新的描述字,代表与客户TCP连接。
close:关闭套接字
#include <unistd.h>
int close(int sockfd);
返回值:成功:0 出错:-1
功能:将套接口做上“已关闭”标记,并立即返回到进程。这个套接字不能在为进程所用:它不能被read或write操作。但TCP将试图发送已排队待发的任何数据,然后按正常的TCP连接终止序列进行操作。
描述字访问计数:
当调用close对套接字进行关闭时,close只是将其参数对应的套接字的引用计数减一,如果引用计数小于等于0,则会引发四分组终止序列。父进程通过fork创建一个子进程,这时候父进程对应的网络套接字的引用计数会+1。
延伸:如果我们想对一个TCP发送FIN,应该用函数shutdown
通过上面的介绍,大家可以写出一个TCP客户与服务器的通信实例吗?建议大家自己动手试一下:
延展:
延展一:getsockname和getpeername函数:返回与套接口关联的本地协议地址或远程协议地址
#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen);
返回值:成功:0 失败:-1
功能:可以通过套接字描述符来获取自己的IP地址和连接对端的IP地址
用到这两个函数的场景:
- 在一个不调用bind的TCP客户上,当connect成功返回后,getsockname返回内核分配给此连接的本地IP地址和本地端口号。
- 再以端口号0调用bind后(通知内核选择本地端口号),getsockname返回由内核分配的本地端口号。
- getsockname可用来获取某个套接口的地址族。
- 在捆绑了一个通配IP地址的TCP服务器上,一旦与客户建立了连接(accept成功返回),就可以调用getsockname来获得分配给此连接的本地IP地址。在这样的调用中套接口描述字必须是已连接套接口描述字,而不是监听套接口的描述字。
- 在服务器端accept成功后,通过getpeername()函数来获取当前连接的客户端的IP地址和端口号。
延展2:计算机字节序
我在这举个最简单的TCP客户端与服务器端实例:
功能:服务器接收客户端发过来的请求消息,建立连接,打印出客户端的ip以及端口号,然后接收客户端发送过来的消息并打印,在原消息前新增hello后发回给客户,客户端打印。注:我这个示例是可以直接运行的,里面用到了select,fork等本文没讲的接口,但后期也会讲。感兴趣的可以先自己搜一下,或者关注我,这样后面更新可以及时看到。
结果展示:
服务端:
客户端:
源码:
util.h
#ifndef __UTIL__
#define __UTIL__
#include <stdio.h>
extern const uint16_t server_port;//端口号使用1024~49151之间
extern const uint16_t listen_num;
extern const uint16_t max_len;
//信号处理函数
void SIGPIPEHandler(int nSig);
void SIGCHLDHandler(int nSig);
//socket包裹函数
int Socket(int family,int type,int protocol);
int Fork();
int doit(int conn_fd,void *_client_addr);
void str_cli(FILE *fp,int sockfd);
#endif
util.c
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include "util.h"
const uint16_t server_port = 6666;//端口号使用1024~49151之间
const uint16_t listen_num = 10;
const uint16_t max_len = 256;
#define min(x, y) ({ \
typeof(x) _min1 = x; \
typeof(y) _min2 = y; \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
#define max(x, y) ({ \
typeof(x) _max1 = x; \
typeof(y) _max2 = y; \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
void SIGPIPEHandler(int nSig)
{
printf("capture a SIGPIPE signal %d\n", nSig);
}
//等待回收子进程,防止生成僵尸进程
void SIGCHLDHandler(int nSig)
{
pid_t pid;
int stat;
while((pid = waitpid(-1,&stat,WNOHANG))>0)
{
printf("child %d terminated\n",pid);
}
return ;
}
//创建ipv4 字节流套接字
int Socket(int family,int type,int protocol)
{
int sockfd;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//0 right -1 error
{
fprintf(stderr, "socket fail value of erron:%d Error message: %s\n", errno,strerror(errno));
return -1;
}
return sockfd;
}
int Fork()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
printf("fork fail errno : %d Error message:%s\n",\
errno,strerror(errno));
return -1;
}
return pid;
}
int doit(int conn_fd,void *_client_addr)
{
struct sockaddr_in * client_addr = (struct sockaddr_in *)_client_addr;
char cli_ip[max_len];
char buf[max_len];
char rbuf[max_len];
int n_write;
inet_ntop(AF_INET,(void *)&client_addr->sin_addr,(void *)cli_ip,sizeof(cli_ip));
printf("connect with client ip:%s port:%u\n",cli_ip,ntohs(client_addr->sin_port));
while(1)
{
//接收客户端发送过来的信息,并将接收到的数据添加信息后发回给数据
if(read(conn_fd,buf,sizeof(buf))<=0)
{
printf("read num <=0\n");
}
printf("recv buf : %s\n",buf);
strcpy(rbuf,"hello ");
strcat(rbuf,buf);
if((n_write = write(conn_fd,rbuf,strlen(rbuf)))<=0)
{
printf("write num <=0\n");
}
else
{
printf("write num = %d\n",n_write);
}
memset(buf,0,sizeof(buf));
memset(rbuf,0,sizeof(rbuf));
}
return 1;
}
void str_cli(FILE *fp,int sockfd)
{
int maxfdp1,stdlneof;
int n_read;
fd_set rset;
char buf[max_len];
char rbuf[max_len];
stdlneof = 0;
int sel_ret;
int sock_use = 1;//用于判断sock套接字是否可用
struct timeval timeout={0,3};
for(;;)
{
FD_ZERO(&rset);//select每次调用后都要清空重置,否则不能检测描述符变化
if(sock_use)
{
FD_SET(sockfd,&rset);//如果套接字在本端已经关闭,继续添加近select会报错,因为套接字已经不存在了
FD_SET(fileno(fp),&rset);
}
maxfdp1 = max(fileno(fp),sockfd) +1;
if((sel_ret = select(maxfdp1,&rset,NULL,NULL,(struct timeval *)&timeout))==-1)
{
printf("select failed errno:%d Error message:%s\n",errno,strerror(errno));
continue;
}
else if(sel_ret)
{
printf("sel_ret %d\n",sel_ret);
if(FD_ISSET(sockfd,&rset))
{
if((n_read = read(sockfd,rbuf,sizeof(rbuf)))<0)
{
printf("read failed errno:%d Error message:%s\n",errno,strerror(errno));
}
else if(n_read == 0)
{
//在这里,如果read返回0,那么该套接字已经关闭应该返回。
close(sockfd);
sock_use = 0;
//FD_CLR(sockfd,&rset);
}
printf("recv len : %d buf : %s\n",n_read,rbuf);
}
if(FD_ISSET(fileno(fp),&rset))
{
if(fgets(buf,sizeof(buf),fp)!=NULL)
{
//stdlneof = 1;
int n_write;
if((n_write = write(sockfd,buf,strlen(buf)))<=0)
{
printf("write num <=0\n");
}
else
{
printf("write num = %d\n",n_write);
}
}
}
}
else
{
continue;
}
sleep(2);
memset(buf,0,sizeof(buf));
memset(rbuf,0,sizeof(rbuf));
}
}
服务器源文件 server.c
/*
1. socket创建字节流套接字
2. bind绑定ip,port
3. listen从主动套接口,转换为监听套接口
4. accept阻塞等待客户链接
5. 通过已连接套接字,获取客户端IP端口号
6. read获取客户发送过来的数据
7. 添加hello write给客户端
*/
#include <stdio.h>
#include <stdint.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>//fork函数头文件
#include <stdlib.h>
#include <signal.h>
#include "util.h"
extern int errno;
int main(int argc,char **argv)
{
//1. 声明ipv4套接字地址结构变量
struct sockaddr_in server_addr,client_addr;
socklen_t addr_len = sizeof(struct sockaddr_in);
bzero(&server_addr,sizeof(struct sockaddr_in));
bzero(&client_addr,sizeof(struct sockaddr_in));
int sockfd,conn_fd;
//网络地址结构中端口和ip都要用网络序进行存储
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons((uint16_t)server_port);
//server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//创建ipv4 字节流套接字
if((sockfd = Socket(AF_INET,SOCK_STREAM,0)) == -1)//0 right -1 error
{
return -1;
}
if(bind(sockfd,(const struct sockaddr *)&server_addr,addr_len) == -1)//0 right -1 error
{
fprintf(stderr, "bind fail value of erron:%d Error message: %s\n", errno,strerror(errno));
return -1;
}
if(listen(sockfd,listen_num)== -1)//0 right -1 error
{
fprintf(stderr, "listen fail value of erron:%d Error message: %s\n", errno,strerror(errno));
return -1;
}
//接收SIGCHLD信号
signal(SIGCHLD,SIGCHLDHandler);
pid_t pid;
while(1)
{
conn_fd = accept(sockfd,(struct sockaddr *)&client_addr,&addr_len);
if(conn_fd == -1)
{
fprintf(stderr, "accept fail value of erron:%d Error message: %s\n", errno,strerror(errno));
continue;
}
if((pid = Fork()) == 0)
{
close(sockfd);//在子进程中关闭监听套接口
doit(conn_fd,&client_addr);//调用处理函数
close(conn_fd);//关闭已连接套接口
exit(0);
}
close(conn_fd);
}
return 0;
}
客户端源文件 client.c
#include <stdio.h>
#include <stdint.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include "util.h"
int main(int argc,char **argv)
{
if(argc != 2)
{
printf("参数错误,请重新执行(参数应为服务器ip)\n");
return -1;
}
//接收处理RST信号
signal(SIGPIPE,SIGPIPEHandler);
int sockfd;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
printf("socket failed errno:%d Error message:%s \n",errno,strerror(errno));
return -1;
}
struct sockaddr_in conn_addr;
bzero((void *)&conn_addr,sizeof(conn_addr));
conn_addr.sin_family = AF_INET;
conn_addr.sin_port = htons(server_port);
if(inet_pton(AF_INET,(const char *)argv[1],(void *)&conn_addr.sin_addr)== 0)
{
printf("inet_pton failed errno:%d Error message:%s\n",errno,strerror(errno));
return -1;
}
if(connect(sockfd,(struct sockaddr *)&conn_addr,(socklen_t)sizeof(conn_addr))==-1)
{
printf("connect failed errno:%d Error message:%s\n",errno,strerror(errno));
return -1;
}
struct timeval timeout = {3,0};
setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(const char *)&timeout,sizeof(timeout));
//FILE *fp = open("hh.txt",O_CREATE)
str_cli(stdin,sockfd);
return 0;
}