Linux 网络编程——TCP编程
概述
TCP(Transmission
Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 具有以下特点:
1)电话系统服务模式的抽象
2)每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程
3)可靠、出错重传、且每收到一个数据都要给出相应的确认,保证数据传输的可靠性
TCP 编程的 C/S 架构
基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:
TCP 客户端编程
创建一个用于网络通信的 socket套接字(描述符),详细用法,请看《套接字的介绍》
family:本示例写 AF_INET,代表 IPv4type:本示例写 SOCK_STREAM,代表 TCP 数据流protocol:这里写 0,设为 0 表示使用默认协议
成功:套接字失败 < 0
int connect( int sockfd,
const struct sockaddr *addr,
socklen_t len );
功能:
主动跟服务器建立连接,有点类似于,我们给别人电话,主动拨对方的电话号码,具体是怎么一个过程,请《connect()、listen()和accept()三者之间的关系》。
参数:
sockfd:socket()返回的套接字
addr:连接的服务器地址结构
len:地址结构体长度
返回值:
成功:0
失败:-1
connect() 函数相当于拨号码,只有拨通号码并且确定对方是自己要找的人(三次握手)才能进行下一步的通信。
ssize_t send(int sockfd,
const void* buf,
size_t nbytes,
int flags);
功能:
发送数据,最后一个参数为 0 时,可以用 write() 替代( send 等同于 write )。注意:不能用 TCP 协议发送 0 长度的数据包。假如,数据没有发送成功,内核会自动重发。
参数:
sockfd: 已建立连接的套接字
buf: 发送数据的地址
nbytes: 发送缓数据的大小(以字节为单位)
flags: 套接字标志(常为 0)
返回值:
成功:成功发送的字节数
失败 < 0
这里通过 Windows 的网络调试助手和虚拟机中的 ubuntu 客户端程序进行通信,网络调试助手下载请点此处。
Windows 的网络调试助手作为 TCP 服务器,接收客户端的请求,调试助手配置如下:
对于 TCP 客户端编程流程,有点类似于打电话过程:找个可以通话的手机( socket() ) -> 拨通对方号码并确定对方是自己要找的人( connect() ) -> 主动聊天( send() 或 write() ) -> 或者,接收对方的回话( recv() 或 read() ) -> 通信结束后,双方说再见挂电话( close() )。
虚拟机中 ubuntu 的 TCP 客户端程序代码:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> int main(int argc, char *argv[]) { unsigned short port = 8080; // 服务器的端口号 char *server_ip = "10.221.20.10"; // 服务器ip地址 if( argc > 1 ) //函数传参,可以更改服务器的ip地址 { server_ip = argv[1]; } if( argc > 2 ) //函数传参,可以更改服务器的端口号 { port = atoi(argv[2]); } int sockfd; sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字 if(sockfd < 0) { perror("socket"); exit(-1); } // 设置服务器地址结构体 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址 server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(port); // 端口 inet_pton(AF_INET, server_ip, &server_addr.sin_addr); // ip // 主动连接服务器 int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(err_log != 0) { perror("connect"); close(sockfd); exit(-1); } char send_buf[512] = {0}; printf("send data to %s:%d\n",server_ip,port); while(1) { printf("send:"); fgets(send_buf,sizeof(send_buf),stdin); // 输入内容 send_buf[strlen(send_buf)-1]='\0'; send(sockfd, send_buf, strlen(send_buf), 0); // 向服务器发送信息 } close(sockfd); return 0; }
运行结果如下:
对于客户端,也是可以接收数据,前提为,客户端先给服务器发送数据。
ssize_t recv(int sockfd,
void *buf,
size_t nbytes,
int flags);
功能:
接收网络数据,默认的情况下,如果没有接收到数据,这个函数会阻塞,直到有数据到来。
参数:
sockfd:套接字
buf: 接收网络数据的缓冲区的地址
nbytes:接收缓冲区的大小(以字节为单位)
flags: 套接字标志(常为 0 )
返回值:
成功:成功接收的字节数
失败 < 0
测试代码如下:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> int main(int argc, char *argv[]) { unsigned short port = 8080; // 服务器的端口号 char *server_ip = "10.221.20.10"; // 服务器ip地址 if( argc > 1 ) //函数传参,可以更改服务器的ip地址 { server_ip = argv[1]; } if( argc > 2 ) //函数传参,可以更改服务器的端口号 { port = atoi(argv[2]); } int sockfd; sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字 if(sockfd < 0) { perror("socket"); exit(-1); } // 设置服务器地址结构体 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址 server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(port); // 端口 inet_pton(AF_INET, server_ip, &server_addr.sin_addr); // ip // 主动连接服务器 int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(err_log != 0) { perror("connect"); close(sockfd); exit(-1); } printf("send data to %s:%d\n",server_ip,port); char send_buf[512] = "Hi, I am Mike."; send(sockfd, send_buf, strlen(send_buf), 0); // 向服务器发送信息 char recv_buf[512] = {0}; recv(sockfd, recv_buf, sizeof(send_buf), 0); // 接收数据 printf("recv_buf ========== %s\n", recv_buf); close(sockfd); return 0; }
运行结果如下:
TCP 服务器编程
做为 TCP 服务器需要具备的条件呢?
- 具备一个可以确知的地址( bind() ):相当于我们要明确知道移动客服的号码,才能给他们电话;
- 让操作系统知道是一个服务器,而不是客户端( listen() ):相当于移动的客服,他们主要的职责是被动接听用户电话,而不是主动打电话骚扰用户;
- 等待连接的到来( accept() ):移动客服时刻等待着,来一个客户接听一个。
接收端使用 bind() 函数,来完成地址结构与
socket 套接字的绑定,这样 ip、port 就固定了,发送端即可发送数据给有明确地址( ip+port ) 的接收端。
int bind( int sockfd,
const struct sockaddr *myaddr,
socklen_t addrlen );
功能:
将本地协议地址与 sockfd 绑定,这样 ip、port 就固定了
参数:
sockfd:socket 套接字
myaddr: 指向特定协议的地址结构指针
addrlen:该地址结构的长度
返回值:
成功:返回 0
失败:-1
使用实例如下:
// 本地网络地址 struct sockaddr_in my_addr; bzero(&my_addr, sizeof(my_addr)); // 清空结构体内容 my_addr.sin_family = AF_INET; // ipv4 my_addr.sin_port = htons(port); // 端口转换 my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定网卡所有ip地址,INADDR_ANY为通配地址,值为0 printf("Binding server to port %d\n", port); int err_log; err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)); // 绑定 if(err_log != 0) { perror("bind"); close(sockfd); exit(-1); }
int listen(int sockfd, int backlog);
功能:
将套接字由主动修改为被动,使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接。更详细说明,请看《connect()、listen()和accept()三者的关系》。
参数:
sockfd: socket监听套接字
backlog:连接队列的长度
返回值:
成功:返回0
失败:其他
int accept( int sockfd,
struct sockaddr *cliaddr,
socklen_t *addrlen );
功能:
从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)。更详细说明,请看《connect()、listen()和accept()三者的关系》。
参数:
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度的地址
返回值:
对于 TCP 服务器编程流程,有点类似于接电话过程:找个可以通话的手机(socket() ) -> 插上电话卡固定一个号码( bind() ) -> 职责为被动接听,给手机设置一个铃声来监听是否有来电( listen() ) -> 有来电,确定双方的关系后,才真正接通不挂电话( accept() ) -> 接听对方的诉说( recv() ) -> 适当给些回话( send() )-> 通信结束后,双方说再见挂电话( close() )。成功:已连接套接字。注意:返回的是一个已连接套接字,这个套接字代表当前这个连接
失败:< 0
ubuntu 中的服务器代码如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { unsigned short port = 8080; // 本地端口 if(argc > 1) { port = atoi(argv[1]); } int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:套接字 if(sockfd < 0) { perror("socket"); exit(-1); } // 设置本地地址结构体 struct sockaddr_in my_addr; bzero(&my_addr, sizeof(my_addr)); // 清空 my_addr.sin_family = AF_INET; // ipv4 my_addr.sin_port = htons(port); // 端口 my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip // 绑定 int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)); if( err_log != 0) { perror("binding"); close(sockfd); exit(-1); } err_log = listen(sockfd, 10); // 监听,监听套接字改为被动 if(err_log != 0) { perror("listen"); close(sockfd); exit(-1); } printf("listen client @port=%d...\n",port); while(1) { struct sockaddr_in client_addr; char cli_ip[INET_ADDRSTRLEN] = ""; socklen_t cliaddr_len = sizeof(client_addr); int connfd; // 等待连接 connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len); if(connfd < 0) { perror("accept"); continue; } inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN); printf("----------------------------------------------\n"); printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port)); char recv_buf[512] = ""; while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 ) // 接收数据 { printf("\nrecv data:\n"); printf("%s\n",recv_buf); } close(connfd); //关闭已连接套接字 printf("client closed!\n"); } close(sockfd); //关闭监听套接字 return 0; }
Windows 的网络调试助手作为 TCP 客户端,给 ubuntu 中的服务器发送数据,运行结果如下:
关闭连接:close()
使用 close() 函数即可关闭套接字,关闭一个代表已连接套接字将导致另一端接收到一个 0 长度的数据包,详情请看《 TCP 四次挥手》。
做服务器时
- 关闭监听套接字( socket()和listen()之后的套接字 )将导致服务器无法接收新的连接,但不会影响已经建立的连接;
- 关闭 accept()返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听( socket()和listen()之后的套接字 )。
做客户端时
通常的情况下,是先关闭客户端,再关闭服务器,如果是先关闭服务器,立马启动服务器是,服务器绑定的端口不会立马释放(如下图),要过 1 分钟左右才会释放,为什么会这样的呢?请看《 TCP 四次挥手》。有没有方法让服务器每次启动都能立即成功?请看《端口复用》。关闭连接就是关闭连接,不意味着其他。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。