一.TCP代码服务端的编写
要设计TCP程序,首先调用逻辑肯定是不变的:
我们还是用这样的开发模式,client直接调用系统接口,server采用自己的封装
服务器serverinit
创建套接字
既然要初始化,那势必要创建套接字
还是socket接口 :
int socket(int domain, int type, int protocol)
返回值:成功返回一个文件描述符,失败返回-1
domain:AF_INET,表示的是通信类型AF_INET代表网络通信AF_UNIX是本地通信
type:SOCK_STREAM面向字节流式的通信,注意前面我们创建udp的套接字的时候用的SOCK_DGRAM面向数据报
protocol:0,只要是IP/TCP协议下的网络通信,前面两个参数传了,第三个参数直接写0就行(它代表协议)
(我们还是用log.hpp来对程序信息进行显示,log.hpp详见博客<网络编程1>)
bind绑定
还是老问题,我们需要将ip和port绑定到sockaddr_in中
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen)
返回值:成功返回0,失败返回-1
sockfd:刚刚创建的套接字_sock
addr:我们需要定义一个addr,其中放上服务器的port和ip,然后将它传到这个参数
addrlen:就是sizeof(addr),可以看做是个固定用法
*区别于udp*等待连接
因为TCP需要建立连接才能通信,所以它要先处于一个等待别人连接它的状态.
也就是将套接字状态设置为监听状态
int listen(int sockfd, int backlog)
返回值:成功返回0,失败返回-1
sockfd:套接字
backlog:这里我们先暂且设置为20,这个值不能太大也不能太小(全连接队列长度,后面在详细解释)
完成
listen的第二个参数目前还无法理解,先暂且不谈
服务器serverstart
首先肯定还是要注意服务器是要一直运行的。
在之前的udp编程中,我们使用netstat -anup命令来查看本机的udp程序,类似的我们可以用netstat -antp来查看tcp程序.
我们用这个命令是可以查看到当前服务器是出于listen状态的.
*获取链接*
因为tcp是面向连接的,所以是要先获取链接才能获得数据的.
获取链接
int accept(int sockfd ,struct sockaddr* addr,socklen_t* addrlen)
返回值:成功返回一个文件描述符(套接字sock),失败返回-1
sockfd:就是刚刚创建并绑定好的套接字
addr:输出型参数,返回获取的ip地址
addrlen:固定写为sizeof(addr),是输入输出型参数
这后面的两个参数和recvfrom的后两个参数是很类似的.
对于accept的返回值,它同样是一个套接字,那么这个套接字是干嘛的呢?
答:accept的套接字就是获取连接之后提供服务的,而原来我们创建并绑定并listen的套接字,它的职责就是专门用来获取新的连接的:
我们将上面创建的套接字改名称为"listensock",将accept到的套接字该名称为"servicesock".
获取链接成功之后就要进行服务了:
我们这里用函数service来进行服务的动作
service
和udp一样,我们还是实现一个echo服务器:也就是把收到的数据再发回客户端
在这里我们要注意的就是read的返回值为0的情况就是:对端的数据已经全部读完了,此时对端就会自动关闭链接,因此就没得东西读了.并且此时我们的服务端也要关闭链接.
这样一来TCP实现的服务器基本就完成了:
快速对服务器进行测试
我们不使用客户端,可以用telnet来直接与服务器建立连接:
如果没有telnet的命令,就需要安装一下:sudo yum -y install telnet
此时在telnet下,按 Ctrl键 + "}]"键,就会有如下显示:
意为,向所连接的服务器发送数据,此时我们输入数据就会被服务器以字符串的形式获取到:
想要退出telnet时,就只需再输入一次 Ctrl键 + "}]"键 进入telnet界面,再输入quit就可以退出.
这样就完成了基本的服务器的测试.
缺陷----无法实现多个客户端连接服务器
但是,此时我们的服务器只能链接一个服务器,当我们用多个telnet连接时,我们会发现我们所发数据是无法同时被服务器接受的.
因为这里的我们实现的服务器是个单进程的,它在同一时间只能连接上一个客户端的,处理完了当前的客户端才能去处理下一个客户端.
二.TCP服务器代码的优化
正如缺陷中所述,当前我们实现的服务器是个单进程循环服务器,它无法同时连接上多个客户端,因此我们需要优化一下它 ,这里的思路也很明确------实现一个多进程版本的服务器,让子进程来提供服务,父进程来建立连接
我们拟出如下的框架,但是又有这样的问题:
如果子进程完成了服务任务,我们需要让它自动退出,但是子进程退出时会进入僵尸状态,是需要被回收的,这就又导致了父进程要等待它,而父进程等待是阻塞式等待,在等待子进程退出时又会进入阻塞状态,这样的连锁反应之下,我们这个服务器在实现多个客户端连接时,其性能就没有什么优越性了(父进程一直处于阻塞状态一个一个等)
因此我们是绝对不能用waitpid这样的方法的.
解决方案:
1.解决僵尸问题
我们可以使用信号忽略的方式来忽略掉子进程的退出信号,这样就能让子进程自动释放资源
2.父进程建立连接,子进程处理服务
文件描述符也是有限的,要让父进程关闭掉提供服务的文件描述符(套接字),子进程关闭提供链接的文件描述符来节省文件描述符资源
理论上来说子进程可以不关闭 listensock ,但是父进程必须要把 servicesock 关闭,不然的话会导致文件描述符资源越来越少,最后没有文件描述符可以用.(文件描述符泄露)
把父进程该做的事给加入,让它处理服务任务:
这样一来就完成了多进程版本的TCP服务器的编写.
三.TCP代码客户端的编写
创建套接字,不需要bind
发起连接
int connect(int sockfd, const struct sockaddr* addr,socklen_t addrlen)
sockfd:当前套接字,表示谁发起的连接
addr:连谁
addrlen:sizeof addr 固定写法
返回值:成功返回0,失败返回-1
发送数据
连接成功之后我们可以发送数据给服务器 :
我们之前使用的sendto和write,其中sendto是给UDP用的,而write和send都可以用于TCP,这里我们试试用send:
ssize_t send(int sockfd, const void *buf,size_t len,int flags)
//它的前三个参数和write是一模一样的
sockfd:谁要发送数据
buf:发什么
len:要发多少(期望发多少数据)
flags:0
返回值:成功返回0,失败返回-1
这里我们提前说一下,读数据也可以用recv:
ssize_t recv(int sockfd,void* buf,size_t len,int flags) //它的前三个参数和read一模一样 sockfd:谁要读数据 buf:读到哪里 len:要读多少(期望读多少数据) flags:0 返回值:成功返回大于0(读到多少个数据),失败返回-1
接收数据