linux网络编程之TCP编程

来源:互联网 发布:windows无法格式化cf 编辑:程序博客网 时间:2024/05/21 15:38

基于TCP(面向连接)的socket编程,分为客户端和服务器端。

客户端的流程如下:

(1)创建套接字(socket)

(2)向服务器发出连接请求(connect)

(3)和服务器端进行通信(send/recv)

(4)关闭套接字

服务器端的流程如下:

(1)创建套接字(socket)

(2)将套接字绑定到一个本地地址和端口上(bind)

(3)将套接字设为监听模式,准备接收客户端请求(listen)

(4)等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)

(5)用返回的套接字和客户端进行通信(send/recv)

(6)返回,等待另一个客户请求。

(7)关闭套接字。

 

这里注意:对于一个虚拟机,他只有一个IP,在TCP模式下,将socket设为是SOCK_STREAM,这样就是双向的,所以作为server端和client端可以同时使用一个IP来传递数据,不会出错,下面的例子就是这样的,server和client既接收又发送。


下面说下自己对TCP的三次握手:

第一次,client端通过connect函数发送请求包(SYN J包)到server端,此时client端的connect函数阻塞。

第二次,server端accept函数接收请求包后,礼尚往来回一个确认包(SYN K ,ACK J+1)给client端,此时server端的acept函数阻塞。

第三次,client端connect函数收到server端确认包,即说明可以连接后,返回一个连接包(ACK K+1)给server端,accept正确返回。


下面是正确官方的解释:

TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手(three-way handshake),过程如下图所示。

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手
:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
一个完整的三次握手也就是:
 请求---应答---再次确认

对应的函数接口:


从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。



建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。

2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。

3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。

4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

对应函数接口如图:


过程如下:

  • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
  • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
  • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
  • 接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACKSYNACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。





而UDP是不可以的,一个IP只能用于一方。

下面通过一个具体例子讲解一下具体的过程和相关的函数。

/*************************************************************************   > File Name: Server.c   > Author: Maoyi  ************************************************************************/  #include<netinet/in.h> // sockaddr_in #include<sys/types.h>  // socket #include<sys/socket.h> // socket #include<stdio.h>    // printf #include<stdlib.h>   // exit #include<string.h>   // bzero   #define SERVER_PORT 8000 #define LENGTH_OF_LISTEN_QUEUE 20 #define BUFFER_SIZE 1024 #define FILE_NAME_MAX_SIZE 512   int main(void) {   // 声明并初始化一个服务器端的socket地址结构   struct sockaddr_in server_addr;   bzero(&server_addr, sizeof(server_addr));   server_addr.sin_family = AF_INET;   //server_addr.sin_addr.s_addr = htons(INADDR_ANY);   server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  server_addr.sin_port = htons(SERVER_PORT);     // 创建socket,若成功,返回socket描述符   int server_socket_fd = socket(PF_INET, SOCK_STREAM, 0);   if(server_socket_fd < 0)   {     perror("Create Socket Failed:");     exit(1);   }   int opt = 1;   setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));     // 绑定socket和socket地址结构   if(-1 == (bind(server_socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))))   {     perror("Server Bind Failed:");     exit(1);   }       // socket监听   if(-1 == (listen(server_socket_fd, LENGTH_OF_LISTEN_QUEUE)))   {     perror("Server Listen Failed:");     exit(1);   }     while(1)   {     // 定义客户端的socket地址结构     struct sockaddr_in client_addr;     socklen_t client_addr_length = sizeof(client_addr);       // 接受连接请求,返回一个新的socket(描述符),这个新socket用于同连接的客户端通信     // accept函数会把连接到的客户端信息写到client_addr中 printf("*****server*****before accept***** \n");    int new_server_socket_fd = accept(server_socket_fd, (struct sockaddr*)&client_addr, &client_addr_length); printf("*****server*****after accept function*****\n");    if(new_server_socket_fd < 0)     {       perror("Server Accept Failed:");       break;     }       // recv函数接收数据到缓冲区buffer中     char buffer[BUFFER_SIZE];     bzero(buffer, BUFFER_SIZE);     if(recv(new_server_socket_fd, buffer, BUFFER_SIZE, 0) < 0)     {       perror("Server Recieve Data Failed:");       break;     }       // 然后从buffer(缓冲区)拷贝到file_name中     char file_name[FILE_NAME_MAX_SIZE+1];     bzero(file_name, FILE_NAME_MAX_SIZE+1);     strncpy(file_name, buffer, strlen(buffer)>FILE_NAME_MAX_SIZE?FILE_NAME_MAX_SIZE:strlen(buffer));     printf("%s\n", file_name);       // 打开文件并读取文件数据     FILE *fp = fopen(file_name, "r");     if(NULL == fp)     {       printf("File:%s Not Found\n", file_name);     }     else    {       bzero(buffer, BUFFER_SIZE);       int length = 0;       // 每读取一段数据,便将其发送给客户端,循环直到文件读完为止       while((length = fread(buffer, sizeof(char), BUFFER_SIZE, fp)) > 0)       {         if(send(new_server_socket_fd, buffer, length, 0) < 0)         {           printf("Send File:%s Failed./n", file_name);           break;         }         bzero(buffer, BUFFER_SIZE);       }         // 关闭文件       fclose(fp);       printf("File:%s Transfer Successful!\n", file_name);     }     // 关闭与客户端的连接     close(new_server_socket_fd);   }   // 关闭监听用的socket   close(server_socket_fd);   return 0; }


/*************************************************************************   > File Name: Client.c   > Author: Maoyi  ************************************************************************/  #include<netinet/in.h>  // sockaddr_in #include<sys/types.h>  // socket #include<sys/socket.h>  // socket #include<stdio.h>    // printf #include<stdlib.h>    // exit #include<string.h>    // bzero   #define SERVER_PORT 8000 #define BUFFER_SIZE 1024 #define FILE_NAME_MAX_SIZE 512   int main() {   // 声明并初始化一个客户端的socket地址结构   struct sockaddr_in client_addr;   bzero(&client_addr, sizeof(client_addr));   client_addr.sin_family = AF_INET;   //client_addr.sin_addr.s_addr = htons(INADDR_ANY);   client_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  client_addr.sin_port = htons(0);     // 创建socket,若成功,返回socket描述符   int client_socket_fd = socket(AF_INET, SOCK_STREAM, 0);   if(client_socket_fd < 0)   {     perror("Create Socket Failed:");     exit(1);   }     // 绑定客户端的socket和客户端的socket地址结构 非必需   if(-1 == (bind(client_socket_fd, (struct sockaddr*)&client_addr, sizeof(client_addr))))   {     perror("Client Bind Failed:");     exit(1);   }     // 声明一个服务器端的socket地址结构,并用服务器那边的IP地址及端口对其进行初始化,用于后面的连接   struct sockaddr_in server_addr;   bzero(&server_addr, sizeof(server_addr));   server_addr.sin_family = AF_INET;   if(inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) == 0)   {     perror("Server IP Address Error:");     exit(1);   }   server_addr.sin_port = htons(SERVER_PORT);   socklen_t server_addr_length = sizeof(server_addr);   printf("*****client*****before connect*****\n");  // 向服务器发起连接,连接成功后client_socket_fd代表了客户端和服务器的一个socket连接   if(connect(client_socket_fd, (struct sockaddr*)&server_addr, server_addr_length) < 0)   {     perror("Can Not Connect To Server IP:");     exit(0);   }   printf("*****client*****after connect *****\n");  // 输入文件名 并放到缓冲区buffer中等待发送   char file_name[FILE_NAME_MAX_SIZE+1];   bzero(file_name, FILE_NAME_MAX_SIZE+1);   printf("Please Input File Name On Server:\t");   scanf("%s", file_name);     char buffer[BUFFER_SIZE];   bzero(buffer, BUFFER_SIZE);   strncpy(buffer, file_name, strlen(file_name)>BUFFER_SIZE?BUFFER_SIZE:strlen(file_name));       // 向服务器发送buffer中的数据   if(send(client_socket_fd, buffer, BUFFER_SIZE, 0) < 0)   {     perror("Send File Name Failed:");     exit(1);   }     // 打开文件,准备写入   FILE *fp = fopen(file_name, "w");   if(NULL == fp)   {     printf("File:\t%s Can Not Open To Write\n", file_name);     exit(1);   }     // 从服务器接收数据到buffer中   // 每接收一段数据,便将其写入文件中,循环直到文件接收完并写完为止   bzero(buffer, BUFFER_SIZE);   int length = 0;   while((length = recv(client_socket_fd, buffer, BUFFER_SIZE, 0)) > 0)   {     if(fwrite(buffer, sizeof(char), length, fp) < length)     {       printf("File:\t%s Write Failed\n", file_name);       break;     }     bzero(buffer, BUFFER_SIZE);   }     // 接收成功后,关闭文件,关闭socket   printf("Receive File:\t%s From Server IP Successful!\n", file_name);   close(fp);   close(client_socket_fd);   return 0; } 


那现在我们来调试下:

首先编译两个文件: gcc server.c -o server   

gcc client.c -o client

接下来就是运行:

./server &                      //&表示在后台运行

./client


那么你会看到以下结果



1---->2  第一次握手

2---->3  第二次握手

3---->4  第三次握手

不知道你理解了不,哈哈哈,反正我是理解了。



下面是网络上的一些资料:

1.有一篇好的文章,对Linux下socket编程的原理和要点说的很清楚:

http://blog.csdn.net/chencheng126/article/details/44260799


2.一些windows网络编程的要点吧。包括大头序,小头序,网络字节序。一些常用的函数等。。

http://blog.csdn.net/chencheng126/article/details/43984947


3.《linux网络编程》宋斌写的,这本书不错。写的清楚,容易懂。是一个比较好的参考资料。粗看了看,有不少收益。


4.Linux网络编程的一些面试题

http://blog.csdn.net/chencheng126/article/details/44407981


5.epoll和select

对于io复用中epoll和select方式介绍的很清楚

http://blog.csdn.net/chencheng126/article/details/44410203


6.Epoll模型和例子

http://blog.csdn.net/chencheng126/article/details/45397229

http://blog.csdn.net/chencheng126/article/details/45479241