网络编程套接字(Socket通信)
本节重点:
1、认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
2、学习socket api的基本用法;
3、能够实现一个简单的udp客户端/服务器;
4、能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);
5、理解tcp服务器建立连接, 发送数据, 断开连接的流程
IP地址:
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
举个例子:唐僧西天取经,总喜欢说一句话,我从东土大唐而来,要到西方大雷音寺拜佛取经。
源IP地址:就相当于东土大唐
目的IP地址:就相当于大雷音寺
端口号(port):
1、端口号(port)是传输层协议的内容.
2、端口号是一个2字节16位的整数;
3、端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
4、一个端口号只能被一个进程占用
例:
当唐僧到达西天后,需要有人拿真经给他,这个人就是阿南和迦叶,阿南和迦叶就相当于端口号。
IP地址标识了硬件的唯一性,IP地址用来确认公网中的唯一一台主机,port用来标识该主机上唯一的进程。
IP+port标识的是全网内唯一的进程
port和进程pid都能唯一标识一个进程,那么他们之间后什么关系呢?
端口号和pid的关系:不是所有的进程都是网络进程,所以不是所有的进程都需要端口号,但是所有的进程都需要pid,只有网络进程才需要端口号,pid相当于身份证号,端口号相当于学号。
这里我们简单了解一下TCP和UDP协议,后面会有博客详细阐述
TCP:
传输层协议
有连接
可靠传输
面向字节流
UDP:
传输层协议
无连接
不可靠传输
面向数据报
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
1、发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
2、接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
3、因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
4、TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
5、如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
总结:我们的电脑一般是小端机,即低地址放低权值的数据,高地址放高权值的数据,而网络中采用大端字节序,所以我们发送数据前,需要将发送的数据转成大端发送。
先发送12 最后发送 cd
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket是创建套接字的接口
实现一个简单的基于socket的UDP通信
由客服端client给服务器server发送消息,server再将消息回显给client.
代码实现
Makefile文件
1 .PHONY:all
2 all:udpClient udpServer
3
4 udpClient:udpClient.cc
5 g++ -o $@ $^ -std=c++11
6 udpServer:udpServer.cc
7 g++ -o $@ $^
8
9 .PHONY:clean
10 clean:
11 rm -f udpClient udpServer
udpServer.hpp文件
1 #pragma once
2
3 #include<iostream>
4 #include<string>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<netinet/in.h>
9 #include<stdlib.h>
10 #include<unistd.h>
11 class udpServer
12 {
13 private:
14 std::string ip;
15 int port;
16 int sock;
17 public:
18 udpServer(std::string _ip="127.0.0.1",int _port=8080)
19 :ip(_ip)
20 ,port(_port)
21 {
22
23 }
24 void initServer()
25 {
26 //socket使创建套接字的接口
27 // AF_INET:是底层使用的某种协议,这里是 ipv4.SOCK_DGRAM:是所创建的套接字的类别,SOCK_DGRAM
28 // 是指UDP.SOCK_STREAM是TCP. 最后一个参数 0 ,是采用的协议,一般默认为0就行 。返回值是一个文件
29 // 描述符,一般从3开始
30 sock=socket(AF_INET,SOCK_DGRAM,0);
31 std::cout<< "sock: "<< sock <<std::endl;
32 struct sockaddr_in local;
33 local.sin_family =AF_INET; //sin.family是指协议家族,这里用ipv4
34 local.sin_port = htons(port); //端口号 ,要主机转一下网络序列
35 local.sin_addr.s_addr=inet_addr(ip.c_str()); //inet_addr 把字符串的ip地址,转为4字节的网络序列
36
37 //bind绑定,是使内存文件与网络信息关联起来
38 //第一个参数sock是刚刚所创建的套接字,第二个参数local是ip+port,第3个是所传地址的长度
39 if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0) // <0 绑定失败
40 {
41 std::cerr<< "bind error!\n"<<std::endl; //cerr标准错误
42 exit(1);
43 }
44 }
45
46 //启动服务器,简单的回应服务器
47 //实现从网络中接收数据,然后把收到的数据在本地打印一下,再把该数据返回给客服端
48 void start()
49 {
50 char msg[64];
51 for(;;)
52 {
53 msg[0]='\0';// 清空字符串
54 struct sockaddr_in end_point; //发送方
55 socklen_t len=sizeof(end_point); //发送方地址大小
56
57 //recvfrom 第一个参数sock是套接字,第二个参数msg是从哪里读,第3个参数是期望读多少,第4个参数设置为
58 //是否非阻塞,为0为阻塞,第5个参数是谁发的,第6个参数是发多少。返回值是读到了多少字节,返回-1为错误
59 ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
60
61 if(s > 0)
62 {
63 msg[s]='\0'; //s中最后一位设置为 '\0'
64 std::cout<<"client# " <<msg <<std::endl;
65 std::string echo_string=msg;
66 echo_string+= " [server echo!]"; //服务器回显
67 //sendto发送,参数与recvfrom 类似
68 sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);
69 }
70 }
71 }
72 ~udpServer()
73 {
74 close(sock);
75 }
76 };
1 #include "udpServer.hpp"
2
3 int main()
4 {
5 udpServer *up=new udpServer();
6 up->initServer();
7 up->start();
8 delete up;
9 return 0;
10 }
~
1 #pragma once
2
3 #include<iostream>
4 #include<string>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<netinet/in.h>
9 #include<stdlib.h>
10 #include<unistd.h>
11 class udpClient
12 {
13 private:
14 std::string ip;
15 int port;
16 int sock;
17 public:
18 //ip,port填谁的,填服务器的
19 udpClient(std::string _ip="127.0.0.1",int _port=8080)
20 :ip(_ip)
21 ,port(_port)
22 {
23
24 }
25 void initClient()
26 {
27 sock=socket(AF_INET,SOCK_DGRAM,0);
28 std::cout<< "sock: "<< sock <<std::endl;
29 }
30
31 //启动服务器,简单的回应服务器
32 void start()
33 {
34 //char msg[64];
35 std::string msg;
36 struct sockaddr_in peer;
37 peer.sin_family=AF_INET;
38 peer.sin_port=htons(port); //服务端port
39 peer.sin_addr.s_addr=inet_addr(ip.c_str()); //服务端的ip
40 for(;;)
41 {
42 std::cout<< "Please Enter# ";
43 std::cin>>msg;
44 if(msg=="quit")
45 {
46 break;
47 }
48 //发送数据
49 sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
50
51 char echo[128]; //接收数据存储区
52
53 ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);
54 if(s>0)
55 {
56 echo[s]=0;
57 std::cout<< "server# "<< echo <<std::endl;
58 }
59 }
60 }
61 ~udpClient()
62 {
63 close(sock);
64 }
65 };
1 #include "udpClient.hpp"
2
3 int main()
4 {
5 udpClient uc;
6 uc.initClient();
7 uc.start();
8 return 0;
9 }
~
实现一个简单的通信:client向服务器发送数据,服务器server收到后,给client一个应答
Makefile文件
1 FLAG=-std=c++11
2
3 .PHONY:all
4 all:tcpClient tcpServer
5
6 tcpClient:tcpClient.cc
7 g++ -o $@ $^ $(FLAG)
8 tcpServer:tcpServer.cc
9 g++ -o $@ $^ $(FLAG)
10
11 .PHONY:clean
12 clean:
13 rm -f tcpClient tcpServer
~
tcpServer.hpp文件
1 #ifndef __TCP_SERVER_H__
2 #define __TCP_SERVER_H__
3
4
5 #include<iostream>
6 #include<string>
7 #include<cstdlib>
8 #include<unistd.h>
9 #include<sys/types.h>
10 #include<sys/socket.h>
11 #include<netinet/in.h>
12 #include<arpa/inet.h>
13 #include<cstring>
14 #define BACKLOG 5
15
16 //***************************************************************
17 //**************** 单进程服务器 *******************************
18 //***************************************************************
19 class tcpServer
20 {
21 private:
22 int port;
23 int lsock; //监听套接字
24 public:
25 tcpServer(int _port)
26 :port(_port)
27 ,lsock(-1)
28 {
29
30 }
31 void initServer()
32 {
33 //创建套接字,创建更多的是文件方面的信息
34 lsock=socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM:流式套接字
35 if(lsock < 0) //文件描述符<0创建失败
36 {
37 std::cerr << "socker error" <<std::endl;
38 exit(2);
39 }
40
41 //填充套接字信息到用户层
42 struct sockaddr_in local;
43 local.sin_family=AF_INET; //ipv4
44 local.sin_port =htons(port); //主机转网络序列
45 local.sin_addr.s_addr=htonl(INADDR_ANY); //主机转网络
46
47 //绑定将套接字填入到内核里,绑定就是将文件信息与网络信息进行绑定
48 if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
49 {
50 std::cerr << "bind error " <<std::endl;
51 exit(3);
52 }
53
54 //将套接字设置为监听状态
55 //listen将套接字的状态设置为监听状态,所谓监听状态就是允许在任
56 //何时刻客服端可以链接我 BACKLOG为全链接的长度
57 if(listen(lsock,BACKLOG) < 0 ) //成功返回0,失败返回-1
58 {
59 std::cerr << "bind error" <<std::endl;
60 exit(4);
61 }
62 }
63 void service(int sock)
64 {
65 char buffer[1024];
66 while(true)
67 {
68 //recv send和read write相当
69 //buffer从哪里读,sizeof(buffer)期望读多少,最后一个参数是选择是否非阻塞,0表示阻塞
70 //返回值是读了多少字节
71 size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
72 if(s > 0) //说明读取成功
73 {
74 buffer[s] = 0;
75 std::cout<< "client# " << buffer <<std::endl;
76
77 //回消息给客服端
78 send(sock,buffer,strlen(buffer),0);
79 }
80 else if(s == 0) //说明对端关闭了
81 {
82 std::cout << "client quit ..." << std::endl;
83 break;
84 }
85 else //s<0 读取失败
86 {
87 std::cout << "recv client data error..." << std::endl;
88 break;
89 }
90 }
91 close(sock);
92 }
93 void start()
94 {
95 sockaddr_in endpoint;
96 while(true)
97 {
98 socklen_t len = sizeof(endpoint);
99 //accept第一个参数是套接字,最后两个参数就对方的ip和长度。返回值是一个文件描述符,失败返回-1
100 //这个返回值和socket返回值的区别是:负责拉客的是lsock,对客人服务的是accept的返回值sock,客人是链接
101 int sock = accept(lsock,(struct sockaddr*)&endpoint,&len);
102
103 if(sock < 0)
104 {
105 std::cerr << "accept error" << std::endl;
106 continue;
107 }
108 std::cout << " get a new link..." << std::endl;
109 //客人来了,要对他服务。对用户提供服务
110 service(sock);
111 }
112 }
113 ~tcpServer()
114 {
115
116 }
117 };
118
119 #endif
tcpServer.cc文件
1 #include "tcpServer.hpp"
2
3 static void Usage(std::string proc)
4 {
5 std::cout << "Usage: " << "prot" << std::endl;
6 std::cout << '\t' << proc << " prot" <<std::endl;
7 }
8
9 int main(int argc,char* argv[])
10 {
11 if(argc != 2)
12 {
13 Usage(argv[0]);
14 exit(1);
15 }
16
17 tcpServer* tp=new tcpServer(atoi(argv[1]));
18 tp->initServer();
19 tp->start();
20 delete tp;
21 return 0;
22 }
tcpClient.hpp文件
1 #ifndef __TCP_CLIENT_H__
2 #define __TCP_CLIENT_H__
3
4 #include<iostream>
5 #include<string>
6 #include<stdlib.h>
7 #include<unistd.h>
8 #include<sys/types.h>
9 #include<sys/socket.h>
10 #include<netinet/in.h>
11 #include<cstring>
12 #include<arpa/inet.h>
13
14 class tcpClient
15 {
16 private:
17 std::string svr_ip; //服务器的ip
18 int svr_port; //服务器的端口号
19 int sock;
20 public:
21 tcpClient(std::string _ip="127.0.0.1",int _port=8080) //127.0.0.1本地环回
22 :svr_ip(_ip)
23 ,svr_port(_port)
24 {
25
26 }
27 void initClient()
28 {
29 //创建套接字 ipv4 ,流式套接字 阻塞 返回值是文件描述符
30 sock = socket(AF_INET, SOCK_STREAM, 0);
31 if(sock < 0 )
32 {
33 std::cerr << "socket error" <<std::endl;
34 exit(2); //进程终止
35 }
36
37 //客服端不需要绑定,客服端在用户主机上跑。让OS去帮我们去绑定
38 //客服端也不需要监听:监听的本质是为了让人随时随地来连我,客服端不需要别人连
39 //客服端也不需要accept
40 //tcp客服端要做的是链接服务器
41
42 //要链接的服务器信息
43 struct sockaddr_in svr;
44 svr.sin_family=AF_INET;
45 svr.sin_port = htons(svr_port);
46 svr.sin_addr.s_addr=inet_addr(svr_ip.c_str());
47
48 //第一个参数,是套接字,connect后两个参数是要链接服务器的信息 ,成功返回0 ,失败返回-1
49 if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
50 {
51 std::cerr << "connect error" << std::endl;
52 }
53
54 }
55
56 void start()
57 {
58 char msg[64];
59
60 while(true)
61 {
62 std::cout << "Please Enter Message# ";
63 fflush(stdout);
64
65 //0是标准输入,从标准输入里读,也就是键盘,读到msg里,期望都sizeof(msg)-1个字节
66 size_t s=read(0,msg,sizeof(msg)-1); //返回值是读了多少字节
67 if(s>0)
68 {
69 msg[s-1]=0;
70 //往sock里写
71 send(sock,msg,strlen(msg),0);
72 ssize_t ss= recv(sock,msg,sizeof(msg)-1,0);
73 if(ss > 0)
74 {
75 msg[ss]=0;
76 std::cout<< " server echo # " << msg <<std::endl;
77 }
78 }
79 }
80 }
81 ~tcpClient()
82 {
83 close(sock);
84 }
85 };
86
87 #endif
tcpClient.cc文件
1 #include "tcpClient.hpp"
2
3 void Usage(std::string proc)
4 {
5 std::cout<< "Usage: \n" << "\t";
6 std::cout<< proc << "svr_ip svr_port" << std::endl;
7
8 }
9
10 int main(int argc,char* argv[])
11 {
12 if(argc !=3)
13 {
14 Usage(argv[0]);
15 exit(1);
16 }
17 tcpClient* tc =new tcpClient(argv[1],atoi(argv[2]));
18 tc->initClient();
19 tc->start();
20 delete tc;
21 return 0;
22 }
Makefile文件
1 FLAG=-std=c++11
2
3 .PHONY:all
4 all:tcpClient tcpServer
5
6 tcpClient:tcpClient.cc
7 g++ -o $@ $^ $(FLAG)
8 tcpServer:tcpServer.cc
9 g++ -o $@ $^ $(FLAG)
10
11 .PHONY:clean
12 clean:
13 rm -f tcpClient tcpServer
tcpServer.hpp文件
1 #ifndef __TCP_SERVER_H__
2 #define __TCP_SERVER_H__
3
4
5 #include<iostream>
6 #include<string>
7 #include<cstdlib>
8 #include<unistd.h>
9 #include<sys/types.h>
10 #include<sys/socket.h>
11 #include<netinet/in.h>
12 #include<arpa/inet.h>
13 #include<cstring>
14 #include<signal.h>
15
16 #define BACKLOG 5
17
18 // **********************************************
19 // ************ 多进程服务器 ******************
20 // **********************************************
21 class tcpServer
22 {
23 private:
24 int port;
25 int lsock; //监听套接字
26 public:
27 tcpServer(int _port)
28 :port(_port)
29 ,lsock(-1)
30 {
31
32 }
33 void initServer()
34 {
35 signal(SIGCHLD,SIG_IGN); //忽略子信号
36 //创建套接字,创建更多的是文件方面的信息
37 lsock=socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM:流式套接字
38 if(lsock < 0) //文件描述符<0创建失败
39 {
40 std::cerr << "socker error" <<std::endl;
41 exit(2);
42 }
43
44 //填充套接字信息到用户层
45 struct sockaddr_in local;
46 local.sin_family=AF_INET; //ipv4
47 local.sin_port =htons(port); //主机转网络序列
48 local.sin_addr.s_addr=htonl(INADDR_ANY); //主机转网络
49
50 //绑定将套接字填入到内核里,绑定就是将文件信息与网络信息进行绑定
51 if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
52 {
53 std::cerr << "bind error " <<std::endl;
54 exit(3);
55 }
56
57 //将套接字设置为监听状态
58 //listen将套接字的状态设置为监听状态,所谓监听状态就是允许在任
59 //何时刻客服端可以链接我 BACKLOG为全链接的长度
60 if(listen(lsock,BACKLOG) < 0 ) //成功返回0,失败返回-1
61 {
62 std::cerr << "bind error" <<std::endl;
63 exit(4);
64 }
65 }
66 void service(int sock)
67 {
68 char buffer[1024];
69 while(true)
70 {
71 //recv send和read write相当
72 //buffer从哪里读,sizeof(buffer)期望读多少,最后一个参数是选择是否非阻塞,0表示阻塞
73 //返回值是读了多少字节
74 size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
75 if(s > 0) //说明读取成功
76 {
77 buffer[s] = 0;
78 std::cout<< "client# " << buffer <<std::endl;
79
80 //回消息给客服端
81 send(sock,buffer,strlen(buffer),0);
82 }
83 else if(s == 0) //说明对端关闭了
84 {
85 std::cout << "client quit ..." << std::endl;
86 break;
87 }
88 else //s<0 读取失败
89 {
90 std::cout << "recv client data error..." << std::endl;
91 break;
92 }
93 }
94 close(sock);
95 }
96 void start()
97 {
98 sockaddr_in endpoint;
99 while(true)
100 {
101 socklen_t len = sizeof(endpoint);
102 //accept第一个参数是套接字,最后两个参数就对方的ip和长度。返回值是一个文件描述符,失败返回-1
103 //这个返回值和socket返回值的区别是:负责拉客的是lsock,对客人服务的是accept的返回值sock,客人是链接
104 int sock = accept(lsock,(struct sockaddr*)&endpoint,&len);
105
106 if(sock < 0)
107 {
108 std::cerr << "accept error" << std::endl;
109 continue;
110 }
111 std::string cli_info = inet_ntoa(endpoint.sin_addr);//4字节ip转为点分10进制ip
112 cli_info += ":";
113 cli_info += std::to_string(ntohs(endpoint.sin_port)); //port转为字符串
114
115 std::cout << " get a new link..." << cli_info << " sock: " << sock << std::endl;
116
117 //让子进程去服务
118 pid_t id = fork();
119 if(id == 0)
120 {
121 //子进程关心的是sock,不关心lsock
122 close(lsock); //不影响父进程的lsock,可以不关闭,最好关闭
123 //客人来了,要对他服务。对用户提供服务
124 service(sock);
125 exit(0);
126 }
127
128 //父进程应该关心lsock,父进程必须关闭sock,因为父进程的主要职责是不断
129 //获取链接,当父进程不断accept获取连接时,一定会带来一个后果,父进程获取的
130 //套接字被子进程拿去服务后,父进程就没用了,这个文件描述符还占着呢,导致父进程
131 //可用的文件描述符越来越少
132 close(sock); //不影响子进程的sock,因为大家用的是不同的文件描述符表
133 }
134 }
135 ~tcpServer()
136 {
137
138 }
139 };
140
141 #endif
tcpServer.cc文件
1 #include "tcpServer.hpp"
2
3 static void Usage(std::string proc)
4 {
5 std::cout << "Usage: " << "prot" << std::endl;
6 std::cout << '\t' << proc << " prot" <<std::endl;
7 }
8
9 int main(int argc,char* argv[])
10 {
11 if(argc != 2)
12 {
13 Usage(argv[0]);
14 exit(1);
15 }
16
17 tcpServer* tp=new tcpServer(atoi(argv[1]));
18 tp->initServer();
19 tp->start();
20 delete tp;
21 return 0;
22 }
tcpClient.hpp文件
1 #ifndef __TCP_CLIENT_H__
2 #define __TCP_CLIENT_H__
3
4 #include<iostream>
5 #include<string>
6 #include<stdlib.h>
7 #include<unistd.h>
8 #include<sys/types.h>
9 #include<sys/socket.h>
10 #include<netinet/in.h>
11 #include<cstring>
12 #include<arpa/inet.h>
13
14 class tcpClient
15 {
16 private:
17 std::string svr_ip; //服务器的ip
18 int svr_port; //服务器的端口号
19 int sock;
20 public:
21 tcpClient(std::string _ip="127.0.0.1",int _port=8080) //127.0.0.1本地环回
22 :svr_ip(_ip)
23 ,svr_port(_port)
24 {
25
26 }
27 void initClient()
28 {
29 //创建套接字 ipv4 ,流式套接字 阻塞 返回值是文件描述符
30 sock = socket(AF_INET, SOCK_STREAM, 0);
31 if(sock < 0 )
32 {
33 std::cerr << "socket error" <<std::endl;
34 exit(2); //进程终止
35 }
36
37 //客服端不需要绑定,客服端在用户主机上跑。让OS去帮我们去绑定
38 //客服端也不需要监听:监听的本质是为了让人随时随地来连我,客服端不需要别人连
39 //客服端也不需要accept
40 //tcp客服端要做的是链接服务器
41
42 //要链接的服务器信息
43 struct sockaddr_in svr;
44 svr.sin_family=AF_INET;
45 svr.sin_port = htons(svr_port);
46 svr.sin_addr.s_addr=inet_addr(svr_ip.c_str());
47
48 //第一个参数,是套接字,connect后两个参数是要链接服务器的信息 ,成功返回0 ,失败返回-1
49 if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
50 {
51 std::cerr << "connect error" << std::endl;
52 }
53
54 }
55
56 void start()
57 {
58 char msg[64];
59
60 while(true)
61 {
62 std::cout << "Please Enter Message# ";
63 fflush(stdout); //刷新缓冲区
64
65 //0是标准输入,从标准输入里读,也就是键盘,读到msg里,期望都sizeof(msg)-1个字节
66 size_t s=read(0,msg,sizeof(msg)-1); //返回值是读了多少字节
67 if(s>0)
68 {
69 msg[s-1]=0;
70 //往sock里写
71 send(sock,msg,strlen(msg),0);
72 ssize_t ss= recv(sock,msg,sizeof(msg)-1,0);
73 if(ss > 0)
74 {
75 msg[ss]=0;
76 std::cout<< " server echo # " << msg <<std::endl;
77 }
78 }
79 }
80 }
81 ~tcpClient()
82 {
83 close(sock);
84 }
85 };
86
87 #endif
tcpClient.cc文件
1 #include "tcpServer.hpp"
2
3 static void Usage(std::string proc)
4 {
5 std::cout << "Usage: " << "prot" << std::endl;
6 std::cout << '\t' << proc << " prot" <<std::endl;
7 }
8
9 int main(int argc,char* argv[])
10 {
11 if(argc != 2)
12 {
13 Usage(argv[0]);
14 exit(1);
15 }
16
17 tcpServer* tp=new tcpServer(atoi(argv[1]));
18 tp->initServer();
19 tp->start();
20 delete tp;
21 return 0;
22 }
Makefile文件
1 FLAG=-std=c++11 -lpthread
2
3 .PHONY:all
4 all:tcpClient tcpServer
5
6 tcpClient:tcpClient.cc
7 g++ -o $@ $^ $(FLAG)
8 tcpServer:tcpServer.cc
9 g++ -o $@ $^ $(FLAG)
10
11 .PHONY:clean
12 clean:
13 rm -f tcpClient tcpServer
~
tcpServer.hpp文件
1 #ifndef __TCP_SERVER_H__
2 #define __TCP_SERVER_H__
3
4
5 #include<iostream>
6 #include<string>
7 #include<cstdlib>
8 #include<unistd.h>
9 #include<sys/types.h>
10 #include<sys/socket.h>
11 #include<netinet/in.h>
12 #include<arpa/inet.h>
13 #include<cstring>
14 #include<signal.h>
15 #include<pthread.h>
16
17 #define BACKLOG 5
18
19 // **********************************************
20 // ************ 多线程服务器 ******************
21 // **********************************************
22 class tcpServer
23 {
24 private:
25 int port;
26 int lsock; //监听套接字
27 public:
28 tcpServer(int _port)
29 :port(_port)
30 ,lsock(-1)
31 {
32
33 }
34 void initServer()
35 {
36 //signal(SIGCHLD,SIG_IGN); //忽略子信号
37 //创建套接字,创建更多的是文件方面的信息
38 lsock=socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM:流式套接字
39 if(lsock < 0) //文件描述符<0创建失败
40 {
41 std::cerr << "socker error" <<std::endl;
42 exit(2);
43 }
44
45 //填充套接字信息到用户层
46 struct sockaddr_in local;
47 local.sin_family=AF_INET; //ipv4
48 local.sin_port =htons(port); //主机转网络序列
49 local.sin_addr.s_addr=htonl(INADDR_ANY); //主机转网络
50
51 //绑定将套接字填入到内核里,绑定就是将文件信息与网络信息进行绑定
52 if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
53 {
54 std::cerr << "bind error " <<std::endl;
55 exit(3);
56 }
57
58 //将套接字设置为监听状态
59 //listen将套接字的状态设置为监听状态,所谓监听状态就是允许在任
60 //何时刻客服端可以链接我 BACKLOG为全链接的长度
61 if(listen(lsock,BACKLOG) < 0 ) //成功返回0,失败返回-1
62 {
63 std::cerr << "bind error" <<std::endl;
64 exit(4);
65 }
66 }
67 static void service(int sock)
68 {
69 char buffer[1024];
70 while(true)
71 {
72 //recv send和read write相当
73 //buffer从哪里读,sizeof(buffer)期望读多少,最后一个参数是选择是否非阻塞,0表示阻塞
74 //返回值是读了多少字节
75 size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
76 if(s > 0) //说明读取成功
77 {
78 buffer[s] = 0;
79 std::cout<< "client# " << buffer <<std::endl;
80
81 //回消息给客服端
82 send(sock,buffer,strlen(buffer),0);
83 }
84 else if(s == 0) //说明对端关闭了
85 {
86 std::cout << "client quit ..." << std::endl;
87 break;
88 }
89 else //s<0 读取失败
90 {
91 std::cout << "recv client data error..." << std::endl;
92 break;
93 }
94 }
95 close(sock);
96 }
97
98 //让线程去提供服务
99 static void* serviceRoutine(void* arg)
100 {
101 pthread_detach(pthread_self()); //线程分离,主线程不必再关心子线程
102
103 std::cout << "create a new thread for IO" <<std::endl;
104 int* p = (int*)arg;
105 int sock = *p;
106 service(sock);
107 delete p;
108
109 }
110 void start()
111 {
112 sockaddr_in endpoint;
113 while(true)
114 {
115 socklen_t len = sizeof(endpoint);
116 //accept第一个参数是套接字,最后两个参数就对方的ip和长度。返回值是一个文件描述符,失败返回-1
117 //这个返回值和socket返回值的区别是:负责拉客的是lsock,对客人服务的是accept的返回值sock,客人是链接
118 int sock = accept(lsock,(struct sockaddr*)&endpoint,&len);
119
120 if(sock < 0)
121 {
122 std::cerr << "accept error" << std::endl;
123 continue;
124 }
125 std::string cli_info = inet_ntoa(endpoint.sin_addr);//4字节ip转为点分10进制ip
126 cli_info += ":";
127 cli_info += std::to_string(ntohs(endpoint.sin_port)); //port转为字符串
128
129 std::cout << " get a new link..." << cli_info << " sock: " << sock << std::endl;
130
131 pthread_t tid;
132 int *p = new int(sock);
133 //创建线程
134 //serviceRoutine线程要执行的函数
135 pthread_create(&tid,nullptr,serviceRoutine,(void*)p);
136 }
137 }
138 ~tcpServer()
139 {
140
141 }
142 };
143
144 #endif
tcpServer.cc文件
130
131 pthread_t tid;
132 int *p = new int(sock);
133 //创建线程
134 //serviceRoutine线程要执行的函数
135 pthread_create(&tid,nullptr,serviceRoutine,(void*)p);
136 }
137 }
138 ~tcpServer()
139 {
140
141 }
142 };
143
144 #endif
tcpClient.hpp文件
1 #ifndef __TCP_CLIENT_H__
2 #define __TCP_CLIENT_H__
3
4 #include<iostream>
5 #include<string>
6 #include<stdlib.h>
7 #include<unistd.h>
8 #include<sys/types.h>
9 #include<sys/socket.h>
10 #include<netinet/in.h>
11 #include<cstring>
12 #include<arpa/inet.h>
13
14 class tcpClient
15 {
16 private:
17 std::string svr_ip; //服务器的ip
18 int svr_port; //服务器的端口号
19 int sock;
20 public:
21 tcpClient(std::string _ip="127.0.0.1",int _port=8080) //127.0.0.1本地环回
22 :svr_ip(_ip)
23 ,svr_port(_port)
24 {
25
26 }
27 void initClient()
28 {
29 //创建套接字 ipv4 ,流式套接字 阻塞 返回值是文件描述符
30 sock = socket(AF_INET, SOCK_STREAM, 0);
31 if(sock < 0 )
32 {
33 std::cerr << "socket error" <<std::endl;
34 exit(2); //进程终止
35 }
36
37 //客服端不需要绑定,客服端在用户主机上跑。让OS去帮我们去绑定
38 //客服端也不需要监听:监听的本质是为了让人随时随地来连我,客服端不需要别人连
39 //客服端也不需要accept
40 //tcp客服端要做的是链接服务器
41
42 //要链接的服务器信息
43 struct sockaddr_in svr;
44 svr.sin_family=AF_INET;
45 svr.sin_port = htons(svr_port);
46 svr.sin_addr.s_addr=inet_addr(svr_ip.c_str());
47
48 //第一个参数,是套接字,connect后两个参数是要链接服务器的信息 ,成功返回0 ,失败返回-1
49 if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
50 {
51 std::cerr << "connect error" << std::endl;
52 }
53
54 }
55
56 void start()
57 {
58 char msg[64];
59
60 while(true)
61 {
62 std::cout << "Please Enter Message# ";
63 fflush(stdout); //刷新缓冲区
64
65 //0是标准输入,从标准输入里读,也就是键盘,读到msg里,期望都sizeof(msg)-1个字节
66 size_t s=read(0,msg,sizeof(msg)-1); //返回值是读了多少字节
67 if(s>0)
68 {
69 msg[s-1]=0;
70 //往sock里写
71 send(sock,msg,strlen(msg),0);
72 ssize_t ss= recv(sock,msg,sizeof(msg)-1,0);
73 if(ss > 0)
74 {
75 msg[ss]=0;
76 std::cout<< " server echo # " << msg <<std::endl;
77 }
78 }
79 }
80 }
81 ~tcpClient()
82 {
83 close(sock);
84 }
85 };
86
87 #endif
tcpClient.cc文件
1 #include "tcpClient.hpp"
2
3 void Usage(std::string proc)
4 {
5 std::cout<< "Usage: \n" << "\t";
6 std::cout<< proc << "svr_ip svr_port" << std::endl;
7
8 }
9
10 int main(int argc,char* argv[])
11 {
12
13 if(argc !=3)
14 {
15 Usage(argv[0]);
16 exit(1);
17 }
18 tcpClient* tc =new tcpClient(argv[1],atoi(argv[2]));
19 tc->initClient();
20 tc->start();
21 delete tc;
22 return 0;
23 }
tcp通信总结
1、单进程:不可使用
2、小型场景应用:
1、多进程版本:健壮性强(进程间具有独立性,一个进程出现问题不影响其它进程),比较吃资源,效率低下
2、多线程版本 :健壮性差,一个线程挂掉,可能导致整个进程挂掉,较吃资源,效率相对低下
大量客服端:系统会存在大量的执行流,切换(调度)可能成为效率低下的重要原因。
文件描述符与套接字的关系:
一个进程创建套接字的时候,本质是创建struct file,得到一个文件描述符,同时还要创建一个struct socket与struct file关联起来,让struct file 里的private_data指向struct socket ,同时让struct socket里的*file回指struct file ,struct socket里面会指向一个tcp_sock