文章目录
- 1. 简单的TCP网络程序
- 1.1 设置监听
- 1.2 服务器提供连接
- 1.3 服务器提供服务
- 1.4 客户端进行连接
- 1.5 本地测试
- 1.6 改进版本
- 1.7 再次测试
1. 简单的TCP网络程序
TCP和UDP差不多,我们还是初始化和启动:
因为客户端和服务器是会有共同的头文件,所以可以放在同一文件中:
既然需要网络通信,我们一定要创建套接字。
因为TCP是面向字节流的,所以使用SOCK_STREAM。这里用的是PF_INET,它和AF_INET是一样的。
套接字创建完成后,我们就可以bind了。
这里ip的意思是如果ip为空,就用这个宏把所有ip都绑上,如有指定ip,就把字符串转成无符号整数类型。
这个inet_aton函数也是将字符串类型转成无符号整数。
然后把这个填好的信息写入内核:
1.1 设置监听
为何要监听呢?
udp是无连接的,tcp是面向连接的。如果想让客户端连接服务器,那么服务器首先要有连接来等待客户端。
这个函数的作用是:将套接字文件描述符从主动转为被动文件描述符,然后用于被动监听客户端的连接。
第二个参数:是一个整数,具体解释后面再说。
监听完了,就可以让别人连接你服务器了。
1.2 服务器提供连接
我们可以使用这个函数来连接:
这里第二个参数是输出型参数,第三个参数是输入输出型参数。让知道是哪个客户端连接的。
返回值:
成功:成功则返回一个新的fd,专门用于与连接成功的客户端进行通信。
失败:返回-1 ,并设置errno。
为什么这里要返回一个新的文件描述符呢?
原来的文件描述符的工作是:获取新的连接(前提有客户端来连接),所以也被叫做监听socket。新返回的文件描述符的工作是:为用户提供网络服务的socket,主要进行IO。
就如图一个是拉客,一个是服务客人一样。
那么成员变量这里我们也应该改准确一点。
1.3 服务器提供服务
我们可以先提供简单的服务,把客户端发送的小写改成大写。
我们知道:TCP是面向字节流的,那么我们该用什么方式来读和写呢?
前面我们在学习文件间通信的时候,用的是read和write,它们就是面向字节流的。
这里s==0的意思是:读到的数据为0,说明对方关闭,client 退出。这里和管道那里的原理是一样的,读端一直在读,写端不写了,并且关闭了写端,读端会读到0,代表对端关闭。
这里strcasecmp的意思是:忽略大小写。如果客户自己输入退出,或者客户端退出,或者读取错误,三种情况会退出循环,结束服务。
如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
到这里,我们可以开始转换大小写了。
这里read和write用的都是同一个文件描述符。因为TCP和UDP支持全双工。
到这里,我们就可以让服务器启动了。
1.4 客户端进行连接
因为客户端需要连接服务器,所以需要服务器的IP和端口号,所以个数没有3个就打印使用手册。有的话就把服务器的IP和端口号取出来。
客户端需要bind吗?
需要,但是不需要自己显示的bind。
需要listen(监听)吗和需要accept(获取连接)吗?
都不需要。
那么创建stocket套接字后,客户端需要做什么呢?
向服务器发起链接请求。
返回值:
这里返回值有个隐藏信息:发起请求,connect 会自动帮我们进行bind!
那么发起请求前,我们要填写信息知道我们是往哪里发送请求的。
下面我们就可以客户端去发送消息了:
定义一个全局的quit。当客户端输入quit后,就退出。
当客户端把消息写入成功后,我们要读取服务器给我们反馈的消息。
1.5 本地测试
这里看到是一个客户端,我们知道一个服务器是可以连多个客户端的,我们测试一下:
可以看到左边的客户端可以进行服务,右边的客户端没有响应。
左边客户端退出了,右边的就显示了。这是为什么呢?
原因:这里是单进程,一旦进入进入transService,主执行流就一直循环执行,只能提供完毕服务之后才能再次进行accept。
1.6 改进版本
我们知道:父进程打开的文件会被子进程继承的。
在子进程中,我们可以把listenSock关闭,防止别人来修改。子进程退出后,父进程一定要把serviceSock关闭,不然父进程能使用的文件描述符会越来越少。
那么现在子进程退出了,父进程需要去等待吗?
如果我们使用了waitpid它默认是阻塞等待,那么就会和上面的情况一样。如果我们使用WNOHANG,会比较麻烦,因为我们需要把每个子进程的pid都记录下来。
方法一:
我们可以让父进程去忽略子进程,这样我们父进程就不需要自己释放了。但是只能在Linux上进行。
方法二:
这里在爸爸进程里再次创建子进程,然后就让爸爸进程退出,这样爷爷进程就不会被阻塞,直接等待成功。那么孙子进程就去执行服务,但是它是一个孤儿进程,就会被OS领养,回收问题就交给了系统了。
但是创建多进程的消耗很大,因为需要创建进程的虚拟地址空间和页表。那么我们就可以用多线程的方式。
方法三:
那么我们该给线程传什么样的参数呢?肯定要有客户端IP、客户端端口号、套接字。
我们把这个指针传给线程,这样线程就能把所有数据给拿到了。
但是这个回调函数是在类部,有一个隐藏的this指针。
但是加上static后,就无法使用类内的其它成员了。
在构造的时候把this传进去。
这样就可以使用了。