前面学习了经典的进程间通信IPC机制,使得同一台计算机上运行的进程可以相互通信,接下来将描述套接字网络IPC接口,进程能够使用该接口和其它进程通信。
IP地址:有两个版本 IPv4和IPv6,没有特殊说明默认为IPv4
端口号
数据链路和IP中的地址,分别指的是MAC地址和IP地址,传输层中也有类似的概念,那就是端口号,端口号用来识别同一台计算机中进行通信的不同应用程序。
特点:
(1)是一个2字节16位的整数
(2)端口号用来标识一个进程,告诉操作系统当前的这个数据要交给哪一个进程来处理
(3)IP地址+端口号能够标识网络上某一台主机的某一个进程
(4)一个端口号只能被一个进程占用
套接字:
在TCP/IP协议中,”IP+地址+TCP/UDP端口号”唯一标示网络通讯中的一个进程,IP地址+端口号”就称为socket。
网络字节序:
网络中要实现通信少不了数据的传输,所以这里就引入了网络字节序的概念。
前面学习了内存中的多字节数据相对于内存地址有大端小端之分(大端:数据的低位在高地址,高位在低地址。小端:数据的低位在低地址,高位在高地址)。网络数据流同样有大端小端之分,TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //主机字节序转为网络字节序,long类型
uint16_t htons(uint16_t hostshort); //主机字节序为网络字节序,short类型
uint32_t ntohl(uint32_t netlong); //网络字节序转为主机字节序,long类型
uint16_t ntohs(uint16_t netshort); //网络字节序转为主机字节序,short类型
如果主机是小端字节这些函数将参数做相应的大小端转换然后返回。如果主机是大端字节,这些函数不做转换,将参数原封不动地返回。
相关函数分析:
(1)创建套接字函数socket
参数:domain表示创建socket的类型,可选类型如下
一般IPv4参数指定AF_NET
type确定套接字的类型,可选类型如下:
对于UDP协议,type参数指定为SOCK_DGRAM,表示面向数据报的协议
对于TCP协议,type参数指定为SOCK_STAREAM,表示面向流的传输协议
参数protocol表示创建的方式,通常为0表示按给定的域和套接字类型选择默认协议。
返回值:
成功返回套接字描述符,出错返回-1
(2)将套接字和地址绑定
作用:将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
参数:sockfd服务器的套接字,也就是socket函数的正确返回值。
addr是socket服务器的地址内容。
虽然函数原型中结构体是sockaddr,但是IPv4选择的结构体是sockaddr_in,包括16位端口号和32位的IP地址。这里类似于mem族函数的参数是void*类型,以便接收各种类型的数据。
bind()成功返回0,失败返回-1
客户端一般不需要调用bind(),服务器也不是必须必须调用bind(),如果不调用内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
(3)数据发送函数
参数:
sockfd:表示当前的socket的fd。
buf:待发送数据的缓冲区。
size:缓冲区的长度。
falgs:调用方式标志位,一般位0,改变flags,将会改变sendto发送的方式
addr:指向目的套接字的地址。
addrlen:所指地址的长度。
(4)数据接收函数
参数与上面函数类似
(5)listen
listen声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态。
简单的UDP网络程序
(6)accept
三次握手完成后,服务器调用accept()接收连接。
addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addr为空表示不关心客户端的地址。
(6)connect
客户端需要调用connect()连接服务器
参数与bind参数形式一致,区别在于bind是自己的地址,而connect是对方的地址。
(7)in_addr转字符串的函数:
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr还可以转换IPv6的in_addr
多线程环境下建议使用后者,这个函数由调用者提供一个缓冲区保存结果可以规避线程安全问题
inet_ntoa(不可重入函数)返回结果在静态存储区,不需要我们手动释放,多次调用结果会覆盖
正如下图所示,第二次的地址会覆盖第一次的地址。(conteos7上测试可能没有问题)
结果:
简单的UDP网络程序
服务器:
客户端:
简单的TCP网络程序
服务器:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7 #include<string.h>
8 #define MAX 128
9 int startup(char*ip,int port)
10 {
11 int sock = socket(AF_INET,SOCK_STREAM,0);
12 if(sock<0)
13 {
14 printf("socket error!\n");
15 exit(2);
16 }//填充结构体进行绑定
17 struct sockaddr_in local;
18 local.sin_family=AF_INET;
19 local.sin_addr.s_addr=inet_addr(ip);
20 local.sin_port=htons(port);
21 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
22 printf("bind error!\n");
23 exit(3);
24 }//设置套接字为监听状态
25 if(listen(sock,5)<0){
26 printf("listen error!\n");
27 exit(4);
28 }
29 return sock;
30 }
31 void service(int sock,char *ip,int port)
32 {
33 char buf[MAX];
34 while(1){
35 buf[0]=0;//每次进来清空缓冲区
36 ssize_t s =read(sock,buf,sizeof(buf)-1);
37 if(s>0){
38 buf[s]=0;
39 printf("[%s:%d] say# %s\n",ip,port,buf);
40 write(sock,buf,strlen(buf));}
41
42 else if(s==0)
43 {
44 printf("client [%s:%d] quit !\n",ip,port);
45 break;
46 }else{
47 printf("read error!\n");
48 break;
49 }
50
51 }
52 }
53 int main(int argc,char *argv[])
54 {
55 if(argc != 3)
56 {
57 printf("Usage: %s [ip] [port]\n",argv[0]);
58 return 1;
59 }
60 int listen_sock = startup(argv[1],atoi(argv[2]));
61 struct sockaddr_in peer;
62 char ipBuf[24];//定义缓冲区
63 for(;;){
64 ipBuf[0]=0;
65 socklen_t len = sizeof(peer);
66 int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
67 if(new_sock<0){
68 printf("accept error!\n");
69 continue;
70
71 }
72 inet_ntop(AF_INET,(const void*)&peer.sin_addr,ipBuf,sizeof(ipBuf));
73 //获得新链接
74 int p = ntohs(peer.sin_port);
75 printf("get a new connect,[%s:%d]\n",ipBuf,p);
76 service(new_sock,ipBuf,p);
77 close(new_sock);
78 }
79
80 return 0;
81 }
客户端:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7 #include<string.h>
8 #define MAX 128
9
10 int main(int argc,char *argv[])
11 {
12 if(argc != 3){
13 printf("Usage:%s [ip] [port]\n",argv[0]);
14 return 1;
15 }
16 int sock = socket(AF_INET,SOCK_STREAM,0);
17 if(sock<0){
18 printf("socket error!\n");
19 return 2;
20 }
21 struct sockaddr_in server;
22 server.sin_family = AF_INET;
23 server.sin_port = htons(atoi(argv[2]));
24 server.sin_addr.s_addr=inet_addr(argv[1]);
25 if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0){
26 printf("connect error!\n");
27 return 3;
28 }
29 char buf[MAX];
30 while(1){
31 printf("please Enter# ");
32 fflush(stdout);
33 ssize_t s = read(0,buf,sizeof(buf)-1);
34 if(s>0){
35 buf[s-1]=0;
36 if(strcmp("quit",buf)==0){
37 printf("client quit!\n");
38 break;
39 }
40 write(sock,buf,strlen(buf));
41 s=read(sock,buf,sizeof(buf)-1);
42 buf[0]=0;
43 printf("Server Echo# %s\n",buf);
44 }
45 }
46 close(sock);
47 return 0;
48 }
编译运行:
查看监听状态:
比较总结多进程版本和多线程版本:
多进程版本:通过每个请求,创建子进程的方式来支持多连接。
优点:
(1)链接来才创建子进程,性能受损,时间慢。
(2)只能服务有限个客户。
(3)调度器有压力,cpu调度压力大影响性能,影响客户端等待时间。
缺点:
(1)可处理多个客户
(2)多进程服务器简单,编写周期短
(3)稳定性强
多线程版本:通过每个请求,创建一个线程的方式支持多连接。
多线程所有的优点都以牺牲稳定性为代价,有可能因线程安全问题导致服务器挂掉。