基于TCP协议的网络程序

简介:

下图是基于TCP协议的客户端/服务器程序的一般流程:

图1.1 TCP协议通讯流程

wKioL1en6e6A3HjZAAFHuY9TmLk995.png

建立链接的过程:

图1.2  建立连接的过程

wKioL1en6hqDOmzlAACZ3XO_LCQ559.png

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用 socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从 connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

数据传输的过程:

建立连接后,TCP 协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务 器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结 果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。


关闭链接的过程:

图1.3 关闭连接的过程

wKioL1en6mHxTIkTAACahabf0hk385.png 

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了 连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段


先看一下需要用到的函数

1
2
3
4
5
6
7
8
9
10
11
NAME
        socket - create an endpoint  for  communication
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  socket( int  domain,  int  type,  int  protocol);
 
DESCRIPTION
        socket() creates an endpoint  for  communication and returns a descriptor.

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协 议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的 传输协议。protocol参数的介绍从略,指定为0即可。

1
2
3
4
5
6
7
8
9
NAME
        bind - bind a name to a socket
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  bind( int  sockfd,  const  struct  sockaddr *addr,
                 socklen_t addrlen);

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。

bind() 的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲 过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定 结构体的长度。我们的程序中对myaddr参数是这样初始化的:

1
2
3
4
bzero(&servaddr,  sizeof (servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡, 每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为 SERV_PORT,我们定义为8000。

1
2
3
4
5
6
7
8
NAME
        listen - listen  for  connections on a socket
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  listen( int  sockfd,  int  backlog);

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务 器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于 连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

1
2
3
4
5
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  accept( int  sockfd,  struct  sockaddr *addr, socklen_t *addrlen);

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者 提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。


服务器使这样子的:

1
2
3
4
5
6
7
8
while  (1) {
     cliaddr_len =  sizeof (cliaddr);
     connfd = accept(listenfd, 
             ( struct  sockaddr *)&cliaddr, &cliaddr_len);
     n = read(connfd, buf, MAXLINE);
     ......
     close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。 accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过 这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。 accept()成功返回一个文件描述符,出错返回-1。


TCP网络程序:

server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。

/*server.c*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
#define MAXLINE 80
#define SERV_PORT 8000
 
int  main( void )
{
     struct  sockaddr_in servaddr, cliaddr; //定义套接字地址
     socklen_t cliaddr_len;
     int  listenfd, connfd;
     char  buf[MAXLINE];
     char  str[INET_ADDRSTRLEN];
     int  i, n;
 
     listenfd = socket(AF_INET, SOCK_STREAM, 0);
 
     bzero(&servaddr,  sizeof (servaddr));  //结构体初始化
     servaddr.sin_family = AF_INET;
     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY 宏
     servaddr.sin_port = htons(SERV_PORT);  //端口号
     
     bind(listenfd, ( struct  sockaddr *)&servaddr,  sizeof (servaddr));
 
     listen(listenfd, 20);
 
     printf ( "Accepting connections ...\n" );
     while  (1) {
         cliaddr_len =  sizeof (cliaddr);
         connfd = accept(listenfd, 
                 ( struct  sockaddr *)&cliaddr, &cliaddr_len);
       //accept()返回时传出客户端的地址和端口号
       
         n = read(connfd, buf, MAXLINE);
         printf ( "received from %s at PORT %d\n" ,
                inet_ntop(AF_INET, &cliaddr.sin_addr, str,  sizeof (str)),
                ntohs(cliaddr.sin_port));
     
         for  (i = 0; i < n; i++)
             buf[i] =  toupper (buf[i]); //用来将字符c转换为大写英文字母
         write(connfd, buf, n);
         close(connfd);
     }
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要 调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器 时端口号都不一样,客户端要连接服务器就会遇到麻烦。

1
2
3
4
5
6
7
8
9
NAME
        connect - initiate a connection on a socket
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  connect( int  sockfd,  const  struct  sockaddr *addr,
                    socklen_t addrlen);

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。


client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。

/*client.c*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
#define MAXLINE 80
#define SERV_PORT 8000
 
int  main( int  argc,  char  *argv[])
{
     struct  sockaddr_in servaddr;
     char  buf[MAXLINE];
     int  sockfd, n;
     char  *str;
     
     if  (argc != 2) {
         fputs ( "usage: ./client message\n" , stderr);
         exit (1);
     }
     str = argv[1];
     
     sockfd = socket(AF_INET, SOCK_STREAM, 0);
 
     bzero(&servaddr,  sizeof (servaddr));
     servaddr.sin_family = AF_INET;
     inet_pton(AF_INET,  "127.0.0.1" , &servaddr.sin_addr);
     servaddr.sin_port = htons(SERV_PORT);
     
     connect(sockfd, ( struct  sockaddr *)&servaddr,  sizeof (servaddr));
 
     write(sockfd, str,  strlen (str));
 
     n = read(sockfd, buf, MAXLINE);
     printf ( "Response from server:\n" );
     write(STDOUT_FILENO, buf, n);
 
     close(sockfd);
     return  0;
}

打开两个终端,依次运行./server和./client tcp

wKioL1en7UmQteD1AACgiKOMgVY980.png



本文转自 七十七快 51CTO博客,原文链接:http://blog.51cto.com/10324228/1835531


相关文章
|
20天前
|
负载均衡 网络协议 算法
|
1天前
|
网络协议
网络通信的基石:TCP/IP协议栈的层次结构解析
在现代网络通信中,TCP/IP协议栈是构建互联网的基础。它定义了数据如何在网络中传输,以及如何确保数据的完整性和可靠性。本文将深入探讨TCP/IP协议栈的层次结构,揭示每一层的功能和重要性。
16 5
|
11天前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
46 3
|
11天前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
36 2
|
14天前
|
网络虚拟化
生成树协议(STP)及其演进版本RSTP和MSTP,旨在解决网络中的环路问题,提高网络的可靠性和稳定性
生成树协议(STP)及其演进版本RSTP和MSTP,旨在解决网络中的环路问题,提高网络的可靠性和稳定性。本文介绍了这三种协议的原理、特点及区别,并提供了思科和华为设备的命令示例,帮助读者更好地理解和应用这些协议。
33 4
|
22天前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
49 13
|
21天前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
6月前
|
机器学习/深度学习 人工智能 网络协议
TCP/IP五层(或四层)模型,IP和TCP到底在哪层?
TCP/IP五层(或四层)模型,IP和TCP到底在哪层?
121 4
|
监控 网络协议 网络架构
IP协议【图解TCP/IP(笔记九)】
IP协议【图解TCP/IP(笔记九)】
148 0
|
域名解析 网络协议
IP协议, TCP协议 和DNS 服务分别是干什么的?
大家好,我是阿萨。昨天讲解了网络四层协议[TCP/IP协议族分为哪4层?]今天我们学习下IP 协议, TCP 协议和DNS 协议分别是干什么的。
297 0
IP协议, TCP协议 和DNS 服务分别是干什么的?
下一篇
无影云桌面