B站就业班视频-对应52课
28_哔哩哔哩_bilibili
上一篇文章,如果再多开一个终端(客户端),发送小写字母,想转换成大写,就没有反应了,关闭这个客户端,服务器也没有丝毫反应,说明上篇代码只能1对1 ,这是不现实的,所以这节课来讲如何实现多个客户端同时访问服务端,服务端能挨个回信息。
原因:
由于accept 和read函数都会阻塞,如当read的时候,不能调用accept接受新的连接,当accept阻塞等待的时候,不能read读数据。
注意,这篇代码只是服务端的,客户端代码跟我昨天文里一样
1.基础思路
socket()创建套接字
bind 将套接字文件描述符,和本地一个IP 端口联系在一起
listen
accept()创建一个连接描述符,一般阻塞在这里;;等待;;
fork () 父进程关闭上述描述符 子进程关闭套接字文件描述符
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include<netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>//将网络大端模式的地址,转化为字符串
//const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);int main() {int sfd = socket(AF_INET, SOCK_STREAM, 0);if (sfd <0){printf("socket fun error/n");return -1;}//定义一个地址结gou体struct sockaddr_in myad;//清空 memorybzero(&myad, sizeof(myad));myad.sin_family =AF_INET;myad.sin_port =htons(8888);myad.sin_addr.s_addr = htonl (INADDR_ANY);int ret = bind(sfd, (struct sockaddr *)&myad, sizeof(myad));if (ret<0) {printf("bind error/n");return -1;}//将socket从主动变为被动(服务器必备),这样可以监听来自客户的请求listen( sfd,128); // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//addr是传出参数,保留客户的地址,所以这是客户地址//定义一个地址结gou体接受客户端地址结构体struct sockaddr_in dst;bzero(&myad, sizeof(myad));dst.sin_family =AF_INET;dst.sin_port =htons(8888);socklen_t dstleng = sizeof(dst);while (1) {//jieshou client connect int newfd = accept (sfd, (struct sockaddr* )&dst, &dstleng);if (newfd <0) {printf("服务端,accept error、\n");return -1;}char addstring[128];memset(addstring, 0x00,128);printf("服务器端,还没有fork,客户端地址是:\n");printf("IP:%s,PORT: %d\n", inet_ntop(AF_INET, &dst.sin_addr.s_addr,addstring,sizeof(addstring)),ntohs(dst.sin_port));//fork child tackle message, father listenpid_t clientpid;clientpid = fork();if( clientpid<0) {printf("fork error\n");return -1;}else if (clientpid>0){//father process ,close message 文件描述符close (newfd);}else if (clientpid ==0){//子进程,打印端口,发现一个子进程1个端口,当然ip共享 printf("child process,IP:%s,PORT: %d\n", inet_ntop(AF_INET, &dst.sin_addr.s_addr,addstring,sizeof(addstring)),ntohs(dst.sin_port));//把客户端发送过来的数据读出来int i =0;int n =0;char buf[1024];while (1){memset(buf,0x00,sizeof(buf));n = read (newfd,buf,sizeof(buf));if (n<=0){printf("有一个客户端已关闭,或者读到的字符为0/n");break;}for (i=0;i<n;i++){buf[i] =toupper(buf[i]);}//把数据传回客户端,变成大写了已经write (newfd, buf, n);}close(newfd);exit(0); //子进程退出,避免再次fork子进程}}
close (sfd);//这行是自己写的,老师有没有这行没看清.
return 0;
}
注意,其实这里面很多函数老师用的是封装过的,,我这里还是写的原始函数
2.本节课其他知识点
(在CSDN上搜索相应知识点自学)
2.1 TCP连接时的 3次握手和4次挥手过程,并且写出其中的数字
2.2 TCP数据格式,IP数据格式
2.3 流量控制
当接受和发送的速度不匹配的时候,(例如:fast sender, slow receiver)解决方案(有很多),着重讲解(滑动窗口)
当socket编程中,我们调用read write函数的时候,是从内核的缓冲区中调集数据,一个是发送缓冲区,另一个是接受缓冲区,被同一个fd(文件描述符)所控制。
2.4 mss 和MTU
MTU:最大传输单元,通信术语,Maximum Transmission Unit, 是指一种通信协议的某一层上面能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡,串口等),这个值如果设置为太大会导致丢包重传的时候重传的数据量较大,图中的最大值是1500,其实是一个经验值。
mss :最大报文长度,只是在建立连接的时候,告诉对方我最大能够接收多少数据,在数据通信的过程中就没有mss了
2.5封装函数
封装函数的优点 --节省重复代码
在封装函数的时候,返回值是-1的时候表示失败了。
errno宏:可以用 man errno(命令)来查询宏名称和对应编号,例如
ECHILD No child processes (POSIX.1-2001).
2.6 errno
老师的讲义,和许多其他文章说,在/usr/include/asm-generic/errno.h文件中包含了所有的编号。但是我cat errno.h 只有一堆注释和以下内容——
#ifndef _ERRNO_H
#define _ERRNO_H 1
#include <features.h>
/* The system-specific definitions of the E* constants, as macros. */
#include <bits/errno.h>
/* When included from assembly language, this header only provides the
E* constants. */
#ifndef __ASSEMBLER__
__BEGIN_DECLS
/* The error code set by various library functions. */
extern int *__errno_location (void) __THROW __attribute_const__;
# define errno (*__errno_location ())
# ifdef __USE_GNU
/* The full and simple forms of the name with which the program was
invoked. These variables are set up automatically at startup based on
the value of argv[0]. */
extern char *program_invocation_name;
extern char *program_invocation_short_name;
#include <bits/types/error_t.h>
# endif /* __USE_GNU */
__END_DECLS
#endif /* !__ASSEMBLER__ */
#endif /* errno.h */)
关于socket编程中,忽略以下两种情况的errno,不认为是失败,而是返回循环,继续尝试
-
- errno =EINTR
像accept ,read这样能够引起阻塞的函数,若被信号打断,由于信号的优先级较高,会优先处理信号,等信号处理完成后,会使accept 或者read解除阻塞,然后返回,此时返回值是-1,设置errno =EINTR
-
- errno = ECONNABORTED
表示连接被打断
*********** ************************ *************
以下块引用内容来自CSDN博客,博主iteye_5425的《几个常见的 Socket 连接错误及原因》
ECONNABORTED
该错误被描述为“software caused connection abort”,即“软件引起的连接中止”。原因在于当服务和客户进程在完成用于 TCP 连接的“三次握手”后,客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中止的连接,服务进程将永远不知道该中止的发生。服务器进程一般可以忽略该错误,直接再次调用accept。/* Linux system */
include/asm-alpha/errno.h:#define ECONNABORTED 53 /* Software caused connection
abort */
include/asm-generic/errno.h:#define ECONNABORTED 103 /* Software caused
connection abort */
include/asm-mips/errno.h:#define ECONNABORTED 130 /* Software caused connection
abort */
*********** ************************ *************
2.7 粘包问题
多次发送,接收端不能分辨第一次发送多少,第二次发送多少。例如:第一次发送30个,第二次发送10个,接受的时候每次最多接20个,那么第一次剩下10个就在第二次接受了。
解决方案
(1)报头+数据
报头设置为4字节,报头里写明了数据长度,这样read函数跳过4字节之后,就知道后面要读多少了,避免了粘包问题。
(2)添加结尾标记。但是需要双方协商,还需要挨个去判断。
(3)数据包定长。例如,约定每个包就是128字节。