首页 > 网络 > 网络编程 > TCP程序设计、并发服务器
2018
03-07

TCP程序设计、并发服务器

TCP回顾

TCP协议为计算机提供一种面向连接的,可靠的传输层通信协议。可靠包括超时重传、流量控制、收到数据给出相应的确认等手段。面向连接意味着通信的两端需要有一个建立连接的过程。TCP把数据看到字节流,每次收发的数据大小不确定,但最终所有的数据都可以收到。以下是TCP和UDP的一个对比差异:

TCP程序设计、并发服务器 - 第1张  | rs232的博客

TCP编程的C/S架构

TCP程序设计、并发服务器 - 第2张  | rs232的博客

TCP客户端编程

socket

UDP套接字

TCP程序设计、并发服务器 - 第3张  | rs232的博客

TCP套接字

TCP程序设计、并发服务器 - 第4张  | rs232的博客

connect

和UDP通信一样,TCP的客户端,也需要知道对端的IP地址和端口号才清楚和谁通信,但与UDP不同的是,使用TCP向服务端通信时需要有一步“发起连接”操作,发起连接的目的是在客户机和服务器之间建立一个点到点的连接。发起连接需要使用connect函数。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

功能:
主动跟服务器建立链接
参数:
sockfd:socket套接字
addr: 连接的服务器地址结构
len: 地址结构体长度
返回值
成功返回0 ,失败返回-1
注意:
connect在成功返回之后表示已经在客户端和服务端之后建立连接,只有连接成功才可以开始收发数据。connect不会产生新的套接字。
直接对不存在的IP地址发起connect将直接返回失败,errno错误信息是Connection refused,但是如果IP地址存在但是对端没有开户服务器程序,则connect将会在一定的时间内反复尝试建立连接,直到超时,一般是75s之后,才会返回错误,errno提示信息是Connection timed out。

收发数据

#include <sys/socket.h>
ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);

功能:
对指定的TCP套接字发送数据
参数:
sockfd: 已建立连接的套接字
buf: 发送数据的地址
nbytes: 发送缓数据的大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:
成功返回发送的字节数,失败返回-1
注意:
不能用TCP协议发送0长度的数据包

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);

功能:
用于接收网络数据
参数:
sockfd:套接字
buf: 接收网络数据的缓冲区的地址
nbytes:接收缓冲区的大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:
成功返回接收到字节数,失败返回-1

TCP服务端编程

TCP的服务端编程与UDP的区分非常大,从编程的角度来说,一个程序如果想作为TCP服务器,则必须具备以下条件:

  • 具备一个可以确知的IP地址和端口
  • 让操作系统知道这是一个服务器,而不是一个客户端
  • 等待连接的到来

对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始。

bind

bind的操作与UDP操作一致。

listen

listen,即监听,是将套接字由主动修改为被动,主动模式是该套接字用于向别人发起连接,被动模式是则该套接字用于等待别人的连接。此时操作系统将为该套接字设置一个连接队列,用来记录所有向该套接字发起连接的请求。

int listen(int sockfd, int backlog);

功能:
在指定的套接字上建立监听,用于等待别人发起连接
参数:
sockfd: socket监听套接字
backlog:连接队列的长度,即能监听的最大个数
返回值:
成功返回0,失败返回-1
注意:
向一个连接未满的套接字发起连接(connect)将阻塞一段时间,而向一个连接已满的套接字发起连接则会立刻返回ECONNREFUSED错误。

accept

该接口用于服务器接受一个连接,与客户端的connect一一对应,accept成功返回之后,连接就已经建立了。

#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);

功能:
从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进程默认会进入睡眠等待(阻塞),除非对套接字描述符设置O_NONBLOCK选项。
参数:
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度的地址,值-结果参数,即做输入也做输出用。
返回值:
成功返回已一个非负的值,代表已连接的套接字描述符,失败返回-1
注意:
这个接口返回时会创建一个新的套接字并返回这个套接字的描述符,这个套接字代表当前这个连接,它的状态将不再是监听状态,而是可以进行收发数据,原来的监听套接字不受影响。

close

关闭套接字,关闭一个代表已连接套接字将导致另一端收到一个0长度的数据包,类似于关闭管道的写端将使读端返回0一样。在服务器端关闭套接字时,如果关闭监听套接字将导致服务器无法接收新的连接,但不会影响已经建立的连接,关闭accept返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听套接字。作客户端时,关闭连接就是关闭连接,意味着客户端主动断开,不意味着其他。

TCP编程示例

服务器端:tcp_server.c

#include "common.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void)
{
//创建套接字
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
err_sys("socket error");
//指定绑定的地址结构
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(9999);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);//绑定到本地的任意一个地址
//绑定本址IP地址与端口
int ret;
ret = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(ret != 0)
err_sys("bind error");
//开始监听
ret = listen(sockfd, 5);
if(ret != 0)
err_sys("listen error");
printf("listening ...n");
while(1)
{
struct sockaddr_in client_addr;
char client_ip[INET_ADDRSTRLEN] = {0};
socklen_t cliaddr_len = sizeof(client_addr);
int connfd;
//接受一个连接请求
connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);//阻塞
if(connfd < 0)
err_sys("accept error");
//打印对端的IP地址和端口号
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("client ip=%s, port=%dn", client_ip, ntohs(client_addr.sin_port));
//循环读取数据,再写回套接字,直到对面关闭连接
char recv_buf[2048] = "";
while(recv(connfd, recv_buf, 2048, 0) > 0)
{
printf("nrecv data:n");
printf("%sn", recv_buf);
send(connfd, recv_buf, strlen(recv_buf), 0);
}
//本地也关闭连接
close(connfd);
printf("client closed!n");
}
//关闭监听套接字
close(sockfd);
return 0;
}

客户端:tcp_client.c

#include "common.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void)
{
//创建套接字
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
err_sys("socket error");
//指定connect地址
struct sockaddr_in dst_addr;
bzero(&dst_addr, sizeof(dst_addr));
dst_addr.sin_family = AF_INET;
dst_addr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &dst_addr.sin_addr);
//发起connect操作
int ret;
ret = connect(sockfd, (struct sockaddr*)&dst_addr, sizeof(dst_addr));
if(ret != 0)
err_sys("connect error");
//读写操作
char send_buf[1024] = {0};
char recv_buf[1024] = {0};
while(1)
{
//发送
printf("send:");
fgets(send_buf, 1024, stdin);
if(strncmp(send_buf, "stop", strlen("stop")) == 0)
break;
send(sockfd, send_buf, strlen(send_buf), 0);
//接收
int ret;
ret = recv(sockfd, recv_buf, 1024, 0);
recv_buf[ret] = '';
printf("recv:%sn", recv_buf);
}
close(sockfd);
return 0;
}

三次握手、四次握手

TCP程序设计、并发服务器 - 第5张  | rs232的博客

建立一个TCP连接需要三次握手,而关闭一个连接需要4次握手。一个TCP包的首部有6个标志位,用于标识该数据包的类型,如下:

TCP程序设计、并发服务器 - 第6张  | rs232的博客
建立连接时,三次握手的时序图如下:

TCP程序设计、并发服务器 - 第7张  | rs232的博客

  1. 客户端调用connect进行主动打开,这将发送一个SYN段用于指明客户端打算连接的服务器端口以及初始序号。
  2. 服务器发回一个ACK段进行确认,包含服务器初始序号的SYN报文段作为应答,同时,将确认序号设置为客户的初始序号加1以对客户的SYN报文段进行确认。确认序号是发送这个ACK的一端所期待的下一个序号。
  3. 客户必须将确认序号设置为服务器的初始序号加1以对服务器的SYN报文段进行确认。

建立一个连接需要三次握手,则终止一个连接则需要4次握手,因为TCP有一个半关闭状态,数据可以从两个方向单独进行关闭。

TCP程序设计、并发服务器 - 第8张  | rs232的博客

  1. 某个应用程序首先调用close执行主动关闭,该端的TCP将发送一个FIN分节,表示数据发送完毕。
  2. 接收到这个FIN的对端执行被动关闭。这个FIN由TCP进行确认,并发送一个确认的ACK包。接收到FIN的一端将在套接字上收到一个文件结束符,表示接收端在这个连接上已无数据可收。
  3. 一段时间后,接收进程将调用close关闭套接字,这导致接收进程也发送一个FIN。
  4. 原发送端(即执行主动关闭那一端)确认这个FIN。

以上过程,每一方都需要一个FIN和ACK,因此通常需要4个分节,但是在某些情况下,步骤1中的FIN可以随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭的那一端,有可能被合并成一个分节。
除了正常关闭之外,TCP还有可能被异常关闭,这时关闭端将发送一个复位报文RST而不是FIN来释放一个连接。异常终止将丢弃任何待发数据并立即发送复位报文RST,RST的接收方会区分另一端执行的是异常关闭还是正常关闭,通过程序指定。RST报文不会导致另一端产生任何响应,另一端根本不进行确认,并且收到RST一端也将终止该连接,将通知应用层连接复位。

TCP状态迁移图

TCP程序设计、并发服务器 - 第9张  | rs232的博客

TCP并发编程

多进程式并发

TCP程序设计、并发服务器 - 第10张  | rs232的博客

多线程式并发

TCP程序设计、并发服务器 - 第11张  | rs232的博客

最后编辑:
作者:rs232
这个作者貌似有点懒,什么都没有留下。

留下一个回复

你的email不会被公开。