简单TCP通信程序的编写
工作原理
由于TCP的协议的服务,他是提供一个可靠的,面向字节流,有连接的传输服务,所以他与UDP协议是有部分不同的,但是还是右部分是相同的。具体工作的步骤如下:
1.TCP服务端通信服务的步骤:
- 创建套接字:先创建一个套接字,用于关联网卡与当前通信进程。
- 绑定地址信息:给服务端绑定地址信息(1.告诉系统收到的哪些数据应该交由当前socket 。2.发送数据到指定的源端地址信息)
- 开始监听:告诉系统当前的套接字可以开始处理连接的请求了。
注意:如果说当前套接字的状态是不可处理连接的请求状态(没有处于listen状态),那么就算是现在有客户端请求连接,那么此时也会被丢弃掉。 如果说,系统当前处于listen状态,那么就会建立连接。服务端会给每个客户端创建一个新的套接字,与指定的客户端进行后续的数据通信。 - 获取新建连接操作句柄:因为服务端会给每个连接成功的客户端创建一个新的套接字,这个新的套接字会只与这个客户进行通信,所以需要返回这个套接字的新的操作句柄。
- 收发数据:不用指定对端地址了,因为新建的套接字只与固定的客户端进行连接,所以新建套接字有完整的五元组。
- 关闭套接字:释放资源。
2.TCP客户端通信的操作步骤:
- 创建套接字:用来关联网卡与当前进程。
- 地址绑定信息:不推荐给客户端绑定地址信息。
原因: ①:一个端口只能被一个进程使用,一旦主动绑定固定端口则客户端程序只能运行一个。 ②:一旦端口出现冲突(要绑定的端口已经被别人绑定了),那么绑定就会失败。 ③:就算不主动绑定,在发送数据的时候,系统也会进行自动绑定合适的地址。 ④:客户端在使用什么地址发送数据给客户端其实并不重要,重要的是与其进行通信。 - 发送数据。
- 接收数据。
- 关闭套接字。
函数接口
TCP通信程序的函数接口和UDP通信程序的函数接口有相同的也有不同的,如下: 1.创建套接字:int socket(int domain,int type,int protocal); 其中:
-
domain:地址域类型,由于我们目前学习是IPV4地址域类型,所以填写AF_INET。 -
type:数据传输方式,因为是TCP协议的通信,所以需要的可以提供面向字节流,可靠的,基于连接的流式套接字,所以填写SOCK_STREAM。 -
protocal:协议类型,对于tcp协议,应该传,IPPORTO_TCP进去,但是我们在type参数上已经传给了流式套接字,所以可以默认写0,在流式套接字类型上,0默认表示为tcp协议。
返回值:成功返回操作句柄,失败返回-1;
2.绑定地址信息:int bind(int sockfd,struct sockaddr* addr,socklen_t len); 其中:
- sockfd:创建套接字成功返回的操作句柄。
- addr:这是一个通用接口,我们使用什么样的地址域类型,就使用什么样的结构体,然后传入的时候,强转就可以了。例如我们上面使用的是AF_INET地址域,那么我们就创建ipv4的结构体去保存地址,则就要创建struct sockaddr_in的结构体。
- len:结构体的长度。
返回值:成功返回0,失败返回-1;
接下来就与UDP通信的函数有点不同了: 3.监听:因为对于TCP,他是一个面向连接的协议,所以不像UDP协议,只要客户端发起请求,就可以进行连接,对于TCP协议,如果说客户端发起请求,服务端处于倾听状态,那么就可以接收请求,并建立连接,去处理,但是如果说服务端此时没有处于倾听状态,此时如果建立连接,那么就会连接失败,会被服务端直接进行丢弃(所以说,这个函数接口是针对于服务端的)。 函数接口:int listen(int sockfd,int backlog); 其中:
- sockfd:表示的是我们创建套接字所返回的操作句柄。
- backlog:当前服务端所能处理的最大的客户端连接请求数量。(就是服务端可以同时并发处理的最大连接的客户端数量)
对于这个最大数量,主要的作用是对于SYN泛洪攻击进行防御。 ①:SYN泛洪攻击:恶意主机伪造ip地址,向服务器发送大量连接建立请求,这个样服务端就会不断的创建大量的通信套接字,如果服务端对创建套接字数量不做限制,那么有可能瞬间资源耗尽,系统崩溃。 ②:解决办法:所以基于这样的一个问题,为了避免系统崩溃,因此服务端这把对同一时刻所能创建的新的套接字的数量做了最大限制,而这个限制的数量就是listen函数接口的第二个参数,有了这个限制之后,此时系统遇到了SYN泛洪攻击,顶多无法处理正常请求,但也不至于让系统崩溃以及所有的连接都断开。有了它之后,即便是现在无法处理正常请求,但是之前的连接都还可以正常通信。
返回值:成功返回0,失败返回-1;
4.客户端发起连接请求:int connect(int sockfd,struct sockaddr*addr,socklen_t len); 其中:
- sockfd:创建套接字所返回操作句柄。
- addr:这是一个通用接口,我们使用什么样的地址域类型,就使用什么样的结构体,然后传入的时候,强转就可以了。例如我们上面使用的是AF_INET地址域,那么我们就创建ipv4的结构体去保存地址,则就要创建struct sockaddr_in的结构体。(其中,这里面应该放有服务端的ip地址以及端口号,还有协议类型)
- len:addr的地址信息长度。
返回值:成功返回0,失败返回-1;
5.服务端获取新建连接句柄:int accept(int sockfd,struct sockaddr* addr,scoklen_t len); 其中:
- sockfd:监听套接字的描述符。
- addr:客户端地址信息。(描述当前获取的新建连接句柄是与那个客户端通信的)
- len:输入输出型参数,指定要获取的地址长度,以及返回实际获取的地址长度。
返回值:成功新建连接的套接字描述符—操作句柄,用于后续与客户端进行通信;失败返回-1。 注意:由此我们可以看出来,TCP服务端在接收客户端请求的时候,如果接收成功,会重新创建一个新的套接字,而这个新建立的套接字就是专门与这个客户端进行通信的,所以原本TCP服务端的套接字就像是一个吧台的门迎,当你去注册好信息之后,会给你专门安排一个客服进行服务。
6.发送数据:size_t send(int sockfd,void* data,int len,int flag); 其中:
- sockfd:创建套接字返回的操作句柄。(而对于服务端来说,一定是经过accept所返回的操作句柄)。
- data:要发送的数据首地址。
- flag:要发送的数据长度。
- flag:标志位-通常置为0,表示阻塞发送。(发送数据,就是吧数据放到发送缓冲区,系统进行封装发送,如果此时发送缓冲区处于饱和状态,那么就阻塞等待)
返回值:成功返回实际发送的数据长度,失败返回-1; 注意:对于发送数据,相对于UDP通信服务的的操作,函数参数少了一点,主要的原因就是,客户端建立好的新的套接字以及地址信息,主要是为了与当前这一个客户端进行通信的,所以说,这些地址信息就已经有了,不需要再传参进去了,直接发送数据就可以了。
7.接收数据:size_t recv(int sockfd,void* buf,int len,int flag); 其中:
- sockfd:创建套接字所返回的操作句柄,同上发送数据。
- buf:一个缓冲区空间首地址,用于放置接收的数据。
- len:想要获取接收数据的长度(最大长度不能超过buf的大小)。
- flag:标志位-通常置为0,表示阻塞接收(socket接收缓冲区没有数据则阻塞)
返回值:成功返回实际获取的数据长度;失败返回-1; 断开连接返回0(当服务端recv返回0时,表示确实没有接收到任何数据,但是大多数情况下表示的是断开连接)。
8.关闭套接字:int close(int fd); 其中:
9.部分关闭连接:int shutdowm(int sockfd,int how); 其中:
- sockfd:创建套接字返回的操作句柄。
- how:要关闭的操作类型
SHUT_RD:关闭读这一半,此时用户不能再从套接字读数据,这个接口接收到的数据都会被丢弃,对等端还不知道这个过程。 SHUT_WR:关闭写这一半,此时用户不能再向套接字中写数据,内核会把缓存中的数据发送数据,接着不会再发送数据,对等端将会知道这一点。此时当对等端试图去读的时候,可能会发送错误 SHUT_RDWR:关闭读和写两半,此时用户不能从套接字中读或者写。
封装TCP socket
我们通过上述接口可以发送,所有的函数操作,都是围绕这创建套接字所返回的操作句柄进行的,所以我们可以对其进行封装,如下: 封装TcpScoket这个类,主要是为了实现TCP通信服务的服务端和客户端的函数接口的组合。
通用服务端的简单实现
由上面代码可知,我们在使用通用服务端的时候,只输入端口号即可(但是端口号不可共用,就是已经被绑定的端口号不能再被绑定),因为我们在上述代码中在绑定地址的时候直接用了0.0.0.0这个ip地址,严格来说,这并不是一个真正意义上的IP地址,它表示的是本台主机上的所有ip地址,如果我们绑定这个ip地址,那么本机就会随机匹配一个已经存在的ip地址给它,主要是用于我们简单实现,才这样做的。
注意: ①:他与本地回环地址不同,本地回环地址是127.0.0.1,它不会跟着网络情况的变化而变化,他代表设备的本地虚拟接口,所以默认是永远不会宕掉的接口。(就是永远不会压力过大而断开连接的接口) ②:主要作用:
- 测试本机网络配置,如果可以ping通127.0.0.1这个ip地址,那么就证明本机ip协议安装没有问题。
- 代替localhost(就是本机服务器)在操作系统中有个配置文件(windows中路径为C:\windows\system32\drivers\etc\hosts,Unix/Linux路径为/etc/hosts)将localhost与127.0.0.1绑定在了一起。
通用客户端的简单实现
在运行通用客户端的时候,由于客户端必须指定服务端的ip地址和端口号,所以在传入参数的时候,要将服务端的ip地址和端口号传入。
通用服务器的弊端以及解决办法
1.弊端:上述通用服务器和客户的弊端是,我们在进行一次通信后,就不能再进行通信了,只能等待第二个客户端与服务器绑定,但是第二个客户端与服务器绑定后也只能进行一次通信就结束了。总结下来的原因为:
- 如果我们将数据的收发操作用循环来进行,那么当前服务端无法与多个客户端同时进行通信服务。
- 如果我们不将数据的收发操作用循环来进行,那么当前服务端与客户进行一次收发操作后,就会循环上去,创建新的套接字,等待与下个客户端进行通信,与下个客户端通信一次完成后,又开始了上述的循环。(就是与每个建立成功的客户端只能通信一次就结束了)
2.解决办法: 这样的弊端与我们想要的不同,我们想要的服务器是不仅可以与多个客户端同时进行通信服务,还可以与连接后的一个客户端进行多次通信。而我们要解决的部分主要是:
- 与一个客户端进行通信的阻塞,不会影响到其他客户端与服务端的通信以及新建连接。(接收数据接口,没有接收到数据就会堵塞,如果此时其他客户端与服务端进行通信和连接,就会出现问题,所以这个问题要解决)
- 获取新建连接不会对其他客户端的通信造成影响。(因为在accept这个函数中,没有新建连接也会堵塞)
由于上面的情况,我们可以有两种解决办法, ①:一种是每个客户端与服务端连接成功后,就创建一个线程,这个线程只进行一个操作,就是和这个连接的客户端进行通信。 ②:另外一种就是每个客户端与服务端连接成功后,就创建一个子进程,这个子进程与这个客户端进行通信。
多线程tcp服务器
1.多线程tcp服务器:每次客户端连接成功,都会创建一个线程,这个线程只干一件事情,就是和这个客户端进行通信。(主线程负责建立连接和申请新的套接字,普通线程负责与其对应的客户端进行通信)
2.多线程tcp服务端的简单实现: 注意事项:
- 将套接字传入给普通线程的时候,要考虑到套接字是否在使用完成后被释放掉(因为在栈上的空间,作用域使用完成后,会自动释放资源,所以我们尽可能的在堆上去开辟空间,最后手动释放资源)。
多进程tcp服务器
1.多进程tcp服务器:当前进程作为一个父进程,用来进行对客户端的连接,如果连接成功,那么就创建一个子进程,该子进程只干一件事情,就是与对应的客户端进行通信。
2.多进程tcp服务器简单实现: 注意: ①:由于进程间的关系是,代码共享,数据独有,所以我们在创建一个子进程的时候,一定要将子进程也有的套接字给释放掉。 ②:因为我们是多进程的服务器,一个子进程对应一个客户端,所以在进行子进程操作的时候,我们并没有在父进程中去等待子进程退出,因为无法知道子进程是何时退出的,所以为了防止僵尸进程的出现,直接用信号,将子进程退出的信号设置为默认忽略。(即子进程退出时直接释放资源,不会成为僵尸进程)
|