TCP通信会用到很多API函数,还有许多杂的知识点。
我们知道,TCP协议通信的双方必须先建立连接,然后才能开始数据的读写。双方都必须为该链接分配必要的内和资源,以管理连接的状态和连接上数据的传输。TCP连接是全双工的,完成数据交换后,通信双方必须断开连接以释放系统资源。
TCP连接是一对一的,可靠的流式服务,这种服务方式体现在:当发送端应用程序连续执行多次写操作时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。即TCP模块发送出的TCP报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系。
在这里顺便提一句,UDP协议则是不可靠的数据报服务。UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操作,否则就会丢包。如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
下图为TCP字节流服务和UDP数据报服务的区别(省略传输层以下细节):
下面我将TCP通信的服务器、客户端大概所用函数API顺序展现如下:
下面是程序实例:其中涉及到部分API会在代码后方加以说明。
一、服务器端程序:ser.c
1.前七行是TCP通信所需要的头文件。
2.socket函数:我们知道在Linux操作系统下,一切皆文件。而socket描述符就是一个可读、可写、可控制、可关闭的文件描述符。
#include
#include
int socket(int domain, int type, int pritocol);
domain参数标识系统用的那个底层协议族,我们示例程序中用的是AF_INET,表示用于ipv4 。
type 指定服务类型,SOCK_STREAM是流式服务,SOCK_UGRAM是数据报服务(用于UDP通信中)。
protocol指在前两个参数确定的情况的 下,再选择一个具体的协议。不过一般这个值把它设置为0,表示默认协议。
函数返回整形的socket文件描述符,失败返回-1,并设置errno.
3.第14行出现一个结构体类型 struct sockaddr_in,结构体成员包括IP地址,端口号等。而通用的socket地址类型是sockaddr。
第17-19行就是对这个结构体中的成员进行赋值,设置协议族,设置端口号,设置通信的IP地址。
4.bind:明名socket:将一个socke(t结构体)与socket地址绑定成为给socket命名。在服务器程序中需要命名,因为只有命名客户端才知道如何连接它,客户端不需要命名,而是采用匿名方式,即使用操作系统自动分配的socket地址。
#include
#incldue
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
bind将my_addr所指的socket地址分配给未命名sockfd文件描述符,addrlen参数支出该socket地址的长度。
bind 成功时返回0,失败时返回-1并设置errron,常见errno是EACCES(被绑定的地址受保护),EADDRINUSE(被绑定的地址使用中)。
5.listen 监听:socket被命名后不能马上接受客户连接,需要使用系统调用来川建一个监听队列以存放待处理的客户连接。
#include
int listen(int sockfd, int backlog);
第一个参数指定坚挺的socke,backlog提示内核监听队列的最大长度。监听队列的长度如果唱过backlog,服务器将不受理新的客户连接,客户端也将收到 CONNECTREFUSED信息。
listen成功返回0,失败返回-1并设置errno。
6.accept接收连接。
#include
#include
int accept(int sockfd, struct sockaddr* addr, socklen_t addrlen);
sockfd参数是执行过listen调用的监听socket,大的人参数用来获取被接受连接的远端socket地址,长度由第3个参数指定。
accept成功时返回一个新的连接socket,该socket地址唯一的标识了被接受的这个连接,服务器可以通过读写该socket来与被接受连接对应的客户端通信。accept失败时返回-1并设置errno。
自此,服务器端在while(1)的控制下可以一直处于等待连接状态。
7,TCP数据读写
#include
#include
ssize_t recv((int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv读取sockfd上的数据,buf、len参数分别指定读缓冲区的位置和大小,flags为数据收发提供了额外的控制,通常设为0即可。recv成功返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此可能需要多次调用recv,才能读取到完整数据。recv可能返回0,意味着通信对方已经关闭连接了。recv出错返回-1并设置errno。
send 往sockfd上写入数据,buf和len参数分贝制定写缓冲区的位置和大小。send成功时返回实际写入的数据长度,失败返回-1并设置errno。
8.关闭连接close
#incldue
int close(int fd);
关闭一个连接实际上是关闭该连接对应的socket,可通过如下关闭普通文件描述符的系统调用来完成。但是close并非立即关闭一个连接,而是将fd的引用计数减1,只有当应用技术fd为0时,才真正的关闭连接。若在多进程中,一次fork系统调用默认将父进程中打开的socket引用计数加1,因此我们在父进程和子进程都需要调用close才能将连接关闭。
二、客户端程序:cli.c
这里注意17-19行绑定的端口号和IP地址,不是客户端自己的,二是将要连接的一端,也就是服务器的端口号和IP。
1.connect发起连接
#incldue
#incldue
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
serv_addr参数是服务器监听的socket地址,addrlen参数指定这个地址的长度。connect成功时返回0,一旦成功建立连接,sockfd就唯一的标识这个连接,客户端可以通过读写sockfd来与服务器通信。connect失败返回-1,并设置errno。
三、编译、运行
运行 :先运行服务器端程序ser,让其开始等待。。。再运行客户端程序cli。即可在客户端输入数据,按回车键,便传送到服务器端,并显示出来。
只要连接不断开,可以一直进行通信,直到客户端发出“end”结束指令,客户端退出。此时服务器端并没有结束,继续等待其他客户端的连接。直到ctr+c强制结束。
总结:以上是最最简单的一个TCP通信编程示例。感兴趣的同学可以继续深入学习如何让一个服务器连接多个客户端,也就是实现并发。还有多进程通信,I/O复用。
还有TCP协议中的一些深层的细节的问题,如三次握手四次挥手还有状态转移等等,将在后面学习中,加以总结,继续更博。