B站就业班视频-对应52课
上一篇文章,如果再多开一个终端(客户端),发送小写字母,想转换成大写,就没有反应了,关闭这个客户端,服务器也没有丝毫反应,说明上篇代码只能1对1 ,这是不现实的,所以这节课来讲如何实现多个客户端同时访问服务端,服务端能挨个回信息。
原因:
由于accept 和read函数都会阻塞,如当read的时候,不能调用accept接受新的连接,当accept阻塞等待的时候,不能read读数据。
注意,这篇代码只是服务端的,客户端代码跟我昨天文里一样
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;
//清空 memory
bzero(&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 listen
pid_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;
}
注意,其实这里面很多函数老师用的是封装过的,,我这里还是写的原始函数?
(在CSDN上搜索相应知识点自学)
当接受和发送的速度不匹配的时候,(例如:fast sender, slow receiver)解决方案(有很多),着重讲解(滑动窗口)
当socket编程中,我们调用read write函数的时候,是从内核的缓冲区中调集数据,一个是发送缓冲区,另一个是接受缓冲区,被同一个fd(文件描述符)所控制。
MTU:最大传输单元,通信术语,Maximum Transmission Unit, ?是指一种通信协议的某一层上面能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡,串口等),这个值如果设置为太大会导致丢包重传的时候重传的数据量较大,图中的最大值是1500,其实是一个经验值。
mss :最大报文长度,只是在建立连接的时候,告诉对方我最大能够接收多少数据,在数据通信的过程中就没有mss了
封装函数的优点 --节省重复代码
在封装函数的时候,返回值是-1的时候表示失败了。
errno宏:可以用 man errno(命令)来查询宏名称和对应编号,例如
ECHILD ?????????No child processes (POSIX.1-2001).
老师的讲义,和许多其他文章说,在/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,不认为是失败,而是返回循环,继续尝试
像accept ,read这样能够引起阻塞的函数,若被信号打断,由于信号的优先级较高,会优先处理信号,等信号处理完成后,会使accept 或者read解除阻塞,然后返回,此时返回值是-1,设置errno =EINTR
表示连接被打断
*********** ??************************ ??*************
以下块引用内容来自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 */
*********** ??************************ ??*************
多次发送,接收端不能分辨第一次发送多少,第二次发送多少。例如:第一次发送30个,第二次发送10个,接受的时候每次最多接20个,那么第一次剩下10个就在第二次接受了。
解决方案
(1)报头+数据
报头设置为4字节,报头里写明了数据长度,这样read函数跳过4字节之后,就知道后面要读多少了,避免了粘包问题。
(2)添加结尾标记。但是需要双方协商,还需要挨个去判断。
(3)数据包定长。例如,约定每个包就是128字节。
?