说到TCP服务器,就不得不提socket编程,我们知道,在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识络通讯中的唯一一个进程,“IP地址+端口号”就称为socket。
在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socket pair就唯一标识一个连接。socket本身有“插座”的意思,因此来描述网络连接的一 对的关 系。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
TCP/IP协议规定:网络数据流规定应采用大端字节序,即低地址存储高位数据,因此网络数据流的地址是由低到高的,此处应包括收取哈发送数据两个过程。
1.socket数据类型及其相关函数
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket。然,各种网络协议的地址格式并不相同,如下图所示:
sockaddr数据结构:
可以看到,各种sockaddr的地址结构前16位都是一样的,都表示整个结构体的长度,IPv4、IPv6和UNIX Domain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNI,这样,只要取 得某种sockaddr结构体的 首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地 址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各 种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换下。
举个例子:bind(listen_sock,(struct sockaddr*)&local,sizeof(local))
下面是基于TCP协议服务器/客户端的一般流程:
服务器调用socket()—创建套接字、bind()—绑定、listen()—监听, 完成初始化后,调accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此 期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞 等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close() 关闭连接,就像写端关闭的管道样,服务器 的 read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何方close()后,连接的两个传输方向都关闭,不能再发送数据了。如果方调用shutdown() 则连接处于半关闭状态,仍可接收对方发来的数据。
下面就来看一下这个服务器和客户端是如何实现的:
首先来到目录下创建server.c和client.c两个文件,在编写一下Makefile文件,然后正式编写server.c:
以上就是server的编写,下面是client 的编写:
好了,现在服务器和客户端已将编写好了,我们来测试一下:
我们在另一个终端进入到目标目录里运行client,并输入文字:
然后来看看客户端是否可以收到client发过来的消息:
server已经成功收到了消息,并且TCP服务器只能被动的接受处理client发起的请求,所以只能是server接受client发来的消息,server并不能给client发消息。
好啦,这就是我编写的简单的TCP服务器/客户端啦,希望各种小伙伴们指出问题,共同学习~