开始复习网络编程这一块,话说有一段时间了,那不多说了,开始吧!
这一节,我们学习的是recv和send函数的使用,如果对初始化socket,绑定,连接,write和read等基本操作不太熟悉的话,可以参考前几篇博文,还有网络字节序和本地字节序的转换,需要注意的点也不少,多写才能熟悉。
recv函数
提供了和read一样的功能,不同的是它多了一个参数
ssize_t recv(int sockfd,void *buf,size_t len,int flags)
主要区别在第四个参数,前面的参数可以说是一样的
recv对应的flags有3个选项:
MSG_PEEK:查看数据,并不从系统缓冲区移走数据
MSG_WAITALL:等待所有数据,等到所有的信息到达时才返回,使用它时,recv返回一直阻塞,直到指定的条件满足时,或者发生错误
MSG_OOB:接受或者发送带外数据
send函数
同理,和write函数的功能一样,主要区别在于第四个参数:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
第四个参数flags,有2个选项:
MSG_DONTROUTE:不查找表,它告诉ip,目的主机在本地网络上,没必要查找表。(一般用在网络诊断和路由程序里面)
MSG_OOB:接受或者发送带外数据
总结一下:
这两个函数就是在基础的read和write上加上了第四个可供选择的参数,增加了一些扩展的功能。
下面我们需要使用到recv的PEEK属性来封装一个读取一整行的函数readline,在写readline之前把还需要使用到的自己封装的readn和writen函数,为了更好地理解read和write的工作原理,我们还是使用自己写的功能一样的readn和writen而不直接用read和write。
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft = count;
ssize_t nread;
char* bufp = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd,bufp,nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nread == 0)
return count-nleft;
bufp += nread; //指针偏移,一般来说是直接偏完count字节,没有出错的话
nleft -= nread; //这一步之前理解错了,只是使得跳出循环,而不是什么多次读取,一般我们这里一次读nleft
}
return count;
}
ssize_t writen(int fd,const void *buf,size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while(nleft > 0)
{
if((nwritten = write(fd,bufp,nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue;
bufp += nwritten;
nleft -= nwritten; //同理,见上,仅仅是为了跳出循环,返回count
}
return count;
}
recv_peek
//recv在read上增加了第四个参数,peek是不清空缓冲区
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
int ret = recv(sockfd,buf,len,MSG_PEEK);
if(ret == -1 && errno == EINTR)
continue;
return ret;
}
}
//这个函数的作用相当是先去瞟一眼缓冲区,看见有多少字符,返回这个字符数
readline
ssize_t readline(int sockfd,void *buf,size_t maxline)
{
int ret;
int nread;
char* bufp = buf; //指针指向buf缓冲区
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd,bufp,nleft);
if(ret < 0)
return ret;
else if(ret == 0)
return ret;
//ret==0,当按下ctrl+d退出时候会发送一个信号终止,没写入字符时按下enter换行,会将换行符发送过去(这个发送过程后面会重点提到),下面for循环中的readn会读取换行符
nread = ret;
int i;
for(i = 0;iif(bufp[i] == '\n') //对等端必须将\n发送过来,不发送过来会导致程序卡死在这里
//这里是识别\n,读取到换行符,也就是一整行了,读完就返回,没收到换行符号时继续下面的代码
{
ret = readn(sockfd,bufp,i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//一般来说程序正常不会走到这里(上面那个for循环中直接return了),我试过把下面这段代码注释掉,也是正常地发送信息
//但是为了程序的严谨性,特殊情况也需要考虑到,当遇到接受数据量超过限制值的时候,这种情况比较特殊,单独拿出来解释下
if(nread > nleft)
exit(EXIT_FAILURE);
nleft -= nread;
ret = readn(sockfd,bufp,nread);
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread;
}
return -1;
}
上面的这两段代码在server和client段均需要包含,因为接受和发送数据的函数调用是一样的
这时候,我们把server端的代码补全一下:
封装一个do_service(int sockfd)
void do_service(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));
int ret = readline(conn,recvbuf,sizeof(recvbuf));
if(ret == -1)
ERR_EXIT("readline");
else if(ret == 0) //ret==0表示啥都接收不到
{
printf("client close,原因可能是关闭了,也可能是连接断了\n");
break;
}
fputs(recvbuf,stdout); //向标准输出打印读到的recvbuf
writen(conn,recvbuf,strlen(recvbuf)); //向客户端回射,说明server已经接收到你的信息了
}
}
main函数:暂时还是使用多进程的方式
int main(void)
{
int listenfd;
if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) <0 )
ERR_EXIT("socket");
struct sockaddr_in servaddr; //ipv4的地址家族
memset(&servaddr , 0 , sizeof(servaddr));
servaddr.sin_family = AF_INET; //暂时我们把AF_INET和PF_INET看成一样,细微的区别
servaddr.sin_port = htons(5188); //指定port为5188,并且将其转换为网络字节序,一个整形占2个字节,用s(hort)
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示使用本机任意地址
int on = 1;
//解决关闭服务器立即重启时候的需要等待TIME_WAIT消失过程,除此之外,还有很多改善socket健壮性的选项,这里暂不讨论
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
ERR_EXIT("setsockopt");
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("bind");
if(listen(listenfd,SOMAXCONN) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
pid_t pid;
while(1)
{
if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
ERR_EXIT("accept");
printf("ip地址是:%s \t 端口是:%d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
pid = fork();
if(pid == -1)
ERR_EXIT("fork");
if(pid == 0) //fork这个函数很特别(一次执行返回两个值,后续深入学习下这个函数),创建成功时它向子进程返回0
{
close(listenfd); //关闭主进程的listenfd
do_service(conn); //操作连接的conn套接字,读取数据或是回射数据,见上
exit(EXIT_SUCCESS); //跳出了上面的循环,那就意味着连接关闭了
}
else
close(conn); //fork返回给父进程的是pid在正真系统中的唯一值,在父进程中和conn无关,直接关闭
}
return 0;
下面写客户端client.c:
直接写main函数
int main()
{
int sock;
if((sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("connect");
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)
{
writen(sock,sendbuf,strlen(sendbuf));
//注意这里用的strlen,只读实际上写入的数据长度(\n字符也是一个有效字符,在没有到底容量限制的时候,
//比如我发送了1023个字节+\n,最后一个字节\n会被截断成为\0)
//这样会导致一系列的问题,下一篇博文单独讨论,这里先实现通信功能即可
for(int i = 0;i < strlen(sendbuf);i++)
{
printf("%c\t",sendbuf[i]);
}
int ret = readline(sock,recvbuf,sizeof(recvbuf));
if(ret == -1)
ERR_EXIT("readline");
else if(ret == 0)
{
printf("client close,原因可能关闭了.\n");
break;
}
fputs(recvbuf,stdout); //打印server回射过来的数据
memset(sendbuf,0,sizeof(sendbuf));
memset(recvbuf,0,sizeof(recvbuf));
}
close(sock);
return 0;
}
最后给两个.c文件加上如下的头文件和ERR_EXIT函数:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
最后就能正常通信了,一个回射多个客户端的server和多个client端,运行截图如下:
下一节讨论在本章代码中需要注意的几点常见的误区,和bug。
所有代码都经测试,可用,如有错误,还请指出,谢谢了。