目录
1.传输层的TCP和UDP
传输层有两个重要的协议——TCP(Transmission ControlProtocol,传输控制协议)和UDP(User Data Protocol,用户数据报议)。
TCP: 相当于生活中的打电话
TCP传输控制协议,相对于UDP,TCP是面向连接的、提供可靠的数据传输服务。同时也是较UDP开销较大的、传输速度较慢的。
TCP提供可靠的、面向连接的数据传输服务。使用TCP通信之前,需要进行“三次握手”建立连接,通信结束后还要使用“四次挥手”断开连接。
UDP: 相当于生活中的发短信
UDP即用户数据报协议,其传输机制决定了它的最大优点——快,同时也决定了它最大的缺点——不可靠、不稳定。
UDP是无连接的,发送数据之前不需要建立连接(TCP需要)。减少了开销和延时。
下图表示TCP/IP各协议之间的关系:
2. Socket
2.1Socket是什么
网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“端口”可以唯一标识主机中的应用程序(进程)。这样利用二元组(ip地址,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展。于是UNIX BSD就发明了socket这种东西, socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而不用自己去进行磁盘读写,内存管理。socket类似,提供了tcp/ip协议的抽象,对外提供了一套接口,把复杂的TCP/IP协议族隐藏在接口后面,对用户来说,就是一组简单的接口。
2.2 Socket通信
在日常生活中,socket通信就相当于:我(客户端)要打电话给朋友(服务器),我拨电话号码(预先得知道服务器的IP地址和端口号),朋友听到电话铃声后接通电话(connect和accept),这时我和朋友就建立起了连接,就可以进行通话了(write和read)等通话结束,挂断电话结束此次交谈(close)。
打电话很简单解释了这工作原理:“open—write/read—close”模式。下面是网络socket通信的基本流程:
2.3 Socket操作的API函数(TCP为例)
1、socket()函数
int socket(int domain,int type,int protocol);
socket函数对应于普通文件的打开操作。 普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符,它唯一标识一个socket。
三个参数:
domain:协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、
SOCK_SEQPACKET等等。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、
IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
2、bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数把一个地址族中的特定地址赋给socket。 例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
三个参数:
sockfd:即socket描述字,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addrlen:对应的是地址的长度。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核。
ipv4对应的是sockaddr_in类型定义:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
ipv6对应的sockaddr_in6类型定义:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
unsigned char s6_addr[16];
};
通用套接字 sockaddr 类型定义:
struct sockaddr {
sa_family_t sa_family; /* 2 bytes address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
}
3、listen()函数
int listen(int sockfd, int backlog);
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
两个参数:
sockefd: socket()系统调用创建的要监听的socket描述字
backlog: 相应socket可以在内核里排队的最大连接个数
4、accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accpet()接受来
自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP链接就建立好。
三个参数:
sockfd: socket函数生成的监听socket描述符
addr:用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等
addrlen: 返回客户端协议地址的长度
accept函数的返回值(client_fd)是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
5、connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服器。如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
三个参数:
sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
addrlen: socket地址的长度
3. Socket客户端和服务器端相互通信
3.1 Socket服务器端
为了操作的灵活性,将监听端口设置成命令行输入,不用进入代码就可以重新指定端口。依次调用socket()—>bind()—>listen()—>accept()函数进行write、read操作,最后close(关闭连接)。代码中用到的getopt_long函数,其用法和功能请goto getopt_long函数的解析
代码如下:
1 #include <stdio.h> //man+函数名可查询包含该函数的头文件
2 #include <sys/types.h>
3 #include <sys/socket.h>
4 #include <string.h>
5 #include <arpa/inet.h>
6 #include <errno.h>
7 #include <unistd.h>i
8 #include <netinet/in.h>
9 #include <stdlib.h>
10 #include <getopt.h>
11
12 #define BUFSIZE 1024 //宏定义缓冲区的大小
13 #define BACKLOG 13 //宏定义相应socket可以在内核里排队的最大连接个数
14 #define MSG_server "Hello! I'm a server."//宏定义回给客户端的消息
15
16
17 void print_usage(char *progname) //打印帮助信息和函数用法提示信息
18 {
19 printf("%s usage:\n",progname);//progname的用法
20 printf("-p(--port):specify server listen port.\n");//指定服务器监听端口
21 printf("-h(--help):print this help information.\n");//打印帮助信息
22 }
23
24 int main(int argc,char **argv)
25 {
26 int rv=-1; //read函数返回读到的字节数
27 int listen_fd=-1; //socket描述符
28 int client_fd=-1; //与返回客户的TCP连接的描述符
29 struct sockaddr_in serv_addr; //ipv4对应的服务器结构体变量
30 struct sockaddr_in cli_addr; //ipv4对应的客户端结构体变量
31 socklen_t cliaddr_len; //客户端协议地址的长度
32 char buf[BUFSIZE];//缓冲区
33 int ch; //getopt_long函数的返回值
34 int port=0; //初始化监听的端口
35 struct option long_option[]= //getopt_long函数的第四个参数,长选项结构体
36 {
37 {"port",required_argument,NULL,'p'},
38 {"help",no_argument,NULL,'h'},
39 {NULL,0,NULL,0},
40 };
41
42 /**********下面这部分代码在实现在命令行输入指定的监听端口************/
43 while((ch=getopt_long(argc,argv,"p:h",long_option,NULL))!=-1)//如果有在命令行加入了选项
44 {
45 switch(ch)
46 {
47 case 'p':
48 port=atoi(optarg);//将指向当前选项参数的指针的字符串转换成int类型
49 break;
50 case 'h':
51 print_usage(argv[0]);//打印帮助信息
52 return 0;
53 }
54 }
55
56 if(!port)//如果没有指定端口号
57 {
58 print_usage(argv[0]);//提示指定端口
59 return 0;
60 }
61
62
63 /*********以下就是服务器端的socket编程********************/
64 listen_fd=socket(AF_INET,SOCK_STREAM,0);//第一步调用socket的函数,返回一个描述符,第一个参数代表采用ipv4,第二个参数是指TCP协议,第三个参数为0表示默认自动选择与第二参数对应的协议。
65 if(listen_fd<0)//判断是否创建socket描述符失败
66 {
67 printf("create socket failure :%s\n",strerror(errno));
68 return -1;
69 }
70 printf("socket create fd[%d]\n",listen_fd);//打印socket描述符
71
72 memset (&serv_addr,0,sizeof(serv_addr)); //将结构体变量内存清零
73 serv_addr.sin_family=AF_INET; //采用ipv4
74 serv_addr.sin_port=htons(port); //htons()函数将服务器监听端口由主机字节序转换成网络字节序
75 serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);//htonl()函数把IP地址从主机字节序转换成网络字节序。INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,也就意味着监听所有的IP地址。
76 if(bind(listen_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr))<0)//调用bind()函数绑定IP地址和端口号,第二个参数为一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核。(兼容IPV4和IPV6)
77 {
78 printf("create socket failure :%s\n",strerror(errno));
79 return -2;
80 }
81 printf("socket[%d] bind on port[%d] for all IP address ok\n",listen_fd,port);//bind一个地址和端口已经成功
82 listen(listen_fd,BACKLOG);//调用listen函数监听该socket
83 while(1)
84 {
85 printf("\nStart waiting and accept new client connect...\n",listen_fd);
86 client_fd=accept(listen_fd,(struct sockaddr*)&cli_addr,&cliaddr_len);//调用accpet()接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。
87 if(client_fd<0)//未生成一个新的描述符
88 {
89 printf("accept new socket failure:%s\n",strerror(errno));
90 return -3;
91 }
92 printf("Accept new client [%s:%d] with fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd);//使用 inet_ntoa() 函数将客户端32位整形的IP地址转换成点分十进制字符串格式的IP地址,调用ntohs()函数将网络字节序的端口号转换成主机字节序的端口号。
93
94 memset(buf,0,sizeof(buf));//清空缓冲区
95 if((rv=read(client_fd,buf,sizeof(buf)))<0)//读失败
96 {
97 printf("Read data from client socket[%d] failure :%s\n",client_fd,strerror(errno));
98 close(client_fd);//把该客户端相应的的socket描述字关闭
99 continue;
100 }
101 else if(rv==0)//与客户端的连接断开
102 {
103 printf("client socket [%d] disconnect\n",client_fd);
104 close(client_fd);
105 continue;
106 }
107 printf("read %d bytes data from client[%d] and echo it back:'%s'\n",rv,client_fd,buf);//读成功
108 if(write(client_fd,MSG_server,strlen(MSG_server))<0)//对客户端嘘寒问暖
109 {
110 printf("Write %d bytes data back to client[%d] failure:%s\n",rv,client_fd,strerror(errno));//对客户端的关心未送达
111 close(client_fd);
112 }
113 sleep(1);
114 close(client_fd);
115 }
116 close(listen_fd); //断开连接
117 }
3.2 Socket客户端
同样和服务器对应,客户端也增加灵活性了,将在命令行输入服务器的IP地址和端口号。依次调用socket()—>connect()函数进行write、read操作,最后close(关闭连接)。
代码如下:
1 #include <stdio.h> //man+函数名可查询包含该函数的头文件
2 #include <sys/types.h>
3 #include <sys/socket.h>
4 #include <string.h>
5 #include <arpa/inet.h>
6 #include <errno.h>
7 #include <unistd.h>
8 #include <netinet/in.h>
9 #include <stdlib.h>
10 #include <getopt.h>
11
12
13 #define BUFSIZE 1024 //宏定义缓冲区长度
14 #define MSG_STR "Hello!I'm a client."//客服端写给服务器的数据
15
16 void print_usage(char *progname) //打印帮助信息和函数用法提示信息
17 {
18 printf("%s usage:\n",progname);
19 printf("-i(--ipaddr):specify server IP address.\n");//指定服务器的IP地址
20 printf("-p(--port):specify server port.\n"); //指定服务器的端口
21 printf("-h(--help):print this help information.\n");//打印帮助信息
22 }
23
24 int main(int argc,char **argv)
25 {
26 int conn_fd=-1; //客户端的socket()创建的描述字
27 int rv=-1; //read函数返回读到的字节数
28 char buf[BUFSIZE]; //缓冲区
29 struct sockaddr_in serv_addr; //声明ipv4对应的服务器结构体变量
30 char *serverIP=NULL; //初始化服务器地址为空
31 int port=0; //初始化端口为0
32 int ch; //getopt_long函数的返回值定义
33 struct option long_option[]=
34 {
35 {"ipaddr",required_argument,NULL,'i'},
36 {"port",required_argument,NULL,'p'},
37 {"help",no_argument,NULL,'h'},
38 {NULL,0,NULL,0},
39
40 };
41 /**********以下代码可以实现在命令行输入指定服务器的IP地址和端口***************/
42 while((ch=getopt_long(argc,argv,"i:p:h",long_option,NULL))!=-1)
43 {
44 switch(ch)
45 {
46 case 'i':
47 serverIP=optarg; //服务器IP地址
48 break;
49 case 'p':
50 port=atoi(optarg);//服务器端口
51 break;
52 case 'h':
53 print_usage(argv[0]);
54 return 0;
55 }
56
57 }
58
59 if(!serverIP || !port) //服务器IP地址和端口错误
60 {
61 print_usage(argv[0]);
62 return 0;
63 }
64
65 /***********下面代码是客户端的socket编程*******************/
66
67 conn_fd=socket(AF_INET,SOCK_STREAM,0);第一步调用socket的函数,返回一个描述符,第一个参数代表采用ipv4,第二个参数是指TCP协议,第三个参数为0,表示默认自动选择与第二参数对应的协议。
68 if(conn_fd<0)判断是否创建socket描述符失败
69 {
70 printf("create socket failure:%s\n",strerror(errno));
71 return -1;
72 }
73
74 memset (&serv_addr,0,sizeof(serv_addr));//将结构体变量内存清零
75 serv_addr.sin_family=AF_INET; //采用ipv4
76 serv_addr.sin_port=htons(port);//htons()函数将服务器端口由主机字节序转换成网络字节序
77 inet_aton(serverIP,&serv_addr.sin_addr);//调用 inet_aton() 函数将点分十进制字符串转换成 32位整形类型
78
79
80 if(connect(conn_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0)//调用connect()函数连接服务器失败
81 {
82 printf("connect to server [%s:%d] failure :%s\n",serverIP,port,strerror(errno));
83 return -1;
84 }
85 if(write(conn_fd,MSG_STR,strlen(MSG_STR))<0)//写入数据MSG_STR失败
86 {
87 printf("Write data to server [%s:%d] failure :%s\n",serverIP,port,strerror(errno));
88 goto cleanup;//跳转到cleanup,并执行相应指令
89 }
90 memset(buf,0,sizeof(buf));//清空缓冲区,准备读数据
91 rv=read(conn_fd,buf,sizeof(buf));//返回从缓冲区读到的字节数
92 if(rv<0)//读数据失败
93 {
94 printf("Read data from server failure: %s\n",strerror(errno));
95 goto cleanup;
96 }
97 else if(rv==0)//连接已断开
98 {
99 printf("Client connect to server get disconnected\n");
100 goto cleanup;
101 }
102 printf("Read %d bytes data from server:'%s'\n",rv,buf);
103
104 cleanup:
105 close(conn_fd);//关闭连接
106
107 }
3.3 聊天记录
server端运行结果:(先运行)
client端运行结果: