- 服务器需要绑定端口号,但是客户端不需要绑定,这是为什么?
- INADDR_ANY
- tcp版本的套接字
- 问题1
- 问题2
- 多进程版本的缺陷
- 多线程版本
- 线程池版本的多线程tcp
- 客户端不需要绑定端口号和ip,但是客户端也有自己的端口号和ip。
- 一台电脑有很多个客户端,如果你想要客户端强行绑定端口号,那么就需要所有的公司进行协商,每个客户端使用不同的端口号。但这是不可能的。如果你强行让客户端绑定端口号,那么就极有可能引起冲突,使得某些客户端启动失败。
- 但是服务器不一样,因为服务器一般只有一个,而且服务器一般是一个公司内部的东西,可以协商。而且服务器的端口号和ip地址必须是确定的,众所周知的,因为一台服务器连接着很多客户端,否则就可能找不到服务器。
- 客户端也需要唯一性,但是不要求确定性。我们可以让操作系统来帮助我们分配端口号。因为端口号资源也有上限(16位),操作系统需要管理端口号。所以哪些端口号没有被使用,只有操作系统知道。
- 客户端也有ip地址和端口号,在recv和send的时候,操作系统会帮助我们自动绑定。
-
绑定ip填0,代表本地ip地址。
-
实际上,服务器的绑定时不需要传入ip地址的。我们将网络地址设置为INADDR_ANY,这个宏表示本地任意的ip地址。因为服务器可能有多张网卡,每张网卡可能连接多个ip地址。这样设置可以在所以的ip地址上监听,直到与某个客户端建立连接时才确定用哪个ip地址。
-
因为ip地址时标识唯一主机的,那么我们通过任一关联该主机的ip地址,应该都可以与该主机通信。但是如果绑定确定的ip,那么就会导致只有通过该ip地址才能与主机通信。
-
这个宏起到一个判定的作用,如果检测到你绑定的ip == INADDR_ANY,那么操作系统收到的所有ip报文都交给服务器。如果绑定具体ip,那么只有从这个ip上来的报文才交给你。
-
一个服务器可以创建很多udp,tcp的套接字。socket也是文件,也需要被管理。
在C、C++中打开一个文件(udp除外),也被称为打开一个流。
telnet ip port #链接到一个服务器tcp版本的套接字
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 9 #define BACKLOG 10 10 11 class TcpServer{ 12 private: 13 int port; 14 int sockfd; 15 public: 16 TcpServer(int _port) 17 : port(_port) 18 , sockfd(-1) 19 {} 20 21 void InitServer(){ 22 sockfd = socket(AF_INET, SOCK_STREAM, 0); 23 //bind 24 struct sockaddr_in local; 25 local.sin_family = AF_INET; 26 local.sin_port = htons(port); 27 local.sin_addr.s_addr = INADDR_ANY; 28 if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0){ 29 std::cerr << "bind error" << std::endl; 30 exit(1); 31 } 32 //listen 33 if(listen(sockfd, BACKLOG) < 0){ 34 std::cerr << "listen error" << std::endl; 35 exit(2); 36 } 37 } 38 39 void Start(){ 40 struct sockaddr_in end_point; 41 socklen_t len = sizeof(end_point); 42 for(;;){ 43 // listen successfully, accept 44 int acc_sock = accept(sockfd, (struct sockaddr*)&end_point, &len); 45 if(acc_sock < 0){ 46 std::cerr << "accept error" << std::endl; 47 continue; 48 } 49 // 连接建立完成, 开始通信 50 std::cout << "get a new link... " << std::endl; 51 Service(acc_sock); 52 } 53 } 54 ~TcpServer(){ 55 close(sockfd); 56 } 57 58 private: 59 void Service(int sockfd){ 60 for(;;){ 61 //ssize_t recv(int sockfd, void *buf, size_t len, int flags); 62 char buf[64] = {0}; 63 ssize_t s = recv(sockfd, buf, sizeof(buf)-1, 0); 64 if(s > 0){ 65 buf[s-1] = 0; 66 std::cout << buf << std::endl; 67 68 //int send(int s, const void *msg, size_t len, int flags); 69 std::string str = buf; 70 // str += "[server] "; 71 send(sockfd, str.c_str(), str.size(), 0); 72 } 73 } 74 } 75 };
1 #pragma once 2 #include3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 12 class TcpClient{ 13 private: 14 std::string ip; 15 int port; 16 int sockfd; 17 18 public: 19 TcpClient(std::string _ip, int _port) 20 :ip(_ip) 21 ,port(_port) 22 {} 23 24 void ClientInit(){ 25 sockfd = socket(AF_INET, SOCK_STREAM, 0); 26 // 创建连接 27 // int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 28 struct sockaddr_in server_sock; 29 server_sock.sin_family = AF_INET; 30 server_sock.sin_port = htons(port); 31 server_sock.sin_addr.s_addr = inet_addr(ip.c_str()); //注意字符串 32 if(connect(sockfd, (struct sockaddr*)&server_sock, sizeof(server_sock)) < 0){ 33 std::cerr << "connect error" << std::endl; 34 exit(1); 35 } 36 } 37 38 void Start(){ 39 char buf[64] = {0}; 40 while(true){ 41 size_t s = read(0, buf, sizeof(buf)-1); 42 if(s > 0){ 43 buf[s-1] = 0; 44 send(sockfd, buf, strlen(buf), 0); 45 size_t ss = recv(sockfd, buf, sizeof(buf)-1, 0); 46 if(ss > 0){ 47 buf[s] = 0; 48 std::cout << "server]$ " << buf << std::endl; 49 } 50 } 51 } 52 } 53 ~TcpClient(){ 54 close(sockfd); 55 } 56 };
1 #include "tcpServer.hpp"
2
3 void Usage(char* proc){
4 std::cout <<"Usage :"<< std::endl;
5 std::cout <<" " << proc << " : port " << std::endl;
6 }
7 int main(int argc, char* argv[]){
8 if(argc != 2){
9 Usage(argv[0]);
10 exit(5);
11 }
12
13 TcpServer* ts = new TcpServer(std::atoi(argv[1]));
14 ts->InitServer();
15 ts->Start();
16
17 delete ts;
18 }
1 #include "tcpClient.hpp"
2
3 void Usage(char* proc){
4 std::cout << "Usage :" << std::endl;
5 std::cout << " " << proc << " ip port" << std::endl;
6 }
7 int main(int argc, char* argv[]){
8 if(argc != 3){
9 Usage(argv[0]);
10 exit(2);
11 }
12
13 TcpClient* tc = new TcpClient(argv[1], std::atoi(argv[2]));
14 tc->ClientInit();
15 tc->Start();
16
17 delete tc;
18 }
- 这就是第一个版本的单进程的tcp套接字。这种写法有很多问题。
我们发现tcp客户端断开之后,重新连接不上。
-
因为我们的服务器不知道客户端已经退出,那么服务器的服务就会卡在某个逻辑中(revc 或者 send)。所以我们需要让服务器知道客户端是否退出,如果退出,让服务器直接结束对该客户端的服务,并且释放对该服务器的套接字资源。
-
我们利用recv的返回值来判断客户端是否退出。如果返回0,那么说明已经客户端已经退出。然后在这个退出的逻辑里面结束服务,并且释放套接字资源。
ctrl + z # 将前台进程放到后台 bg 任务号 # 改成running状态 jobs # 查看任务问题2
我们发现当有多个客户端想连接服务器时,只有第一个服务器可以正常使用。
-
这是因为单进程的服务器会导致所有任务共用一份资源,如果一个任务卡死,就会导致所有任务卡死。
-
所以第一个客户端将服务器卡在了Service的死循环中,导致其他的客户端无法连上服务器。
-
我们使用多进程来编写服务器,这样父进程主要用来接收连接,然后fork子进程去完成通信。
-
基于以上,我们的多进程版本的tcp服务器如下:
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 10 #define BACKLOG 10 11 12 class TcpServer{ 13 private: 14 int port; 15 int sockfd; 16 public: 17 TcpServer(int _port) 18 : port(_port) 19 , sockfd(-1) 20 {} 21 22 void InitServer(){ 23 sockfd = socket(AF_INET, SOCK_STREAM, 0); 24 //bind 25 struct sockaddr_in local; 26 local.sin_family = AF_INET; 27 local.sin_port = htons(port); 28 local.sin_addr.s_addr = INADDR_ANY; 29 if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0){ 30 std::cerr << "bind error" << std::endl; 31 exit(1); 32 } 33 //listen 34 if(listen(sockfd, BACKLOG) < 0){ 35 std::cerr << "listen error" << std::endl; 36 exit(2); 37 } 38 } 39 40 void Start(){ 41 signal(SIGCHLD, SIG_IGN); 42 43 struct sockaddr_in end_point; 44 socklen_t len = sizeof(end_point); 45 for(;;){ 46 // listen successfully, accept 47 int acc_sock = accept(sockfd, (struct sockaddr*)&end_point, &len); 48 if(acc_sock < 0){ 49 std::cerr << "accept error" << std::endl; 50 continue; 51 } 52 // 连接建立完成, 开始通信 53 std::cout << "get a new link... " << std::endl; 54 if(fork() == 0){ 55 close(sockfd); 56 Service(acc_sock); 57 exit(6); //子进程退出 58 } 59 60 close(acc_sock); //关闭子进程的sock 61 } 62 } 63 ~TcpServer(){ 64 close(sockfd); 65 } 66 67 private: 68 void Service(int sockfd){ 69 char buf[64] = {0}; 70 for(;;){ 71 //ssize_t recv(int sockfd, void *buf, size_t len, int flags); 72 ssize_t s = recv(sockfd, buf, sizeof(buf)-1, 0); 73 if(s > 0){ 74 buf[s] = 0; 75 std::cout << buf << std::endl; 76 77 //int send(int s, const void *msg, size_t len, int flags); 78 std::string str = buf; 79 // str += "[server] "; 80 send(sockfd, str.c_str(), str.size(), 0); 81 } 82 else if(s == 0){ 83 std::cout << "client quit..." << std::endl; 84 break; 85 } 86 else{ 87 std::cerr << "recv error" << std::cout; 88 break; 89 } 90 } 91 } 92 };
- 多进程的服务器还是有很多细节的。
- 我们知道子进程和父进程会各自拥有独立的文件描述符数组,但是子进程的文件描述符数组信息跟父进程的一样。对于子进程来说,sockfd套接字没有任何作用,因为它只需要accept函数的返回值的acc_sock即可与客户端完成通信。所以最好在子进程中关闭sockfd。而对于父进程,acc_sock没有任何意义,必须关闭acc_sock,不然就可能导致套接字资源的泄露。
- 子进程完成于客户端的通信任务后,子进程退出,那么需要父进程来处理它的退出信息。但是如果父进程使用wait函数来等待子进程退出,那么父进程仍会卡住,这与我们的想法背道而驰。
- 我们的处理方法是,子进程完成工作后,不仅仅会将退出信息交给父进程,还会向父进程发送一个SIGCHLD信号,我们定义处理该信号的方式为忽略即可。
- 一个小技巧:在多进程版本的服务器中,我们可以让子进程再继续fork出孙子进程,然后让子进程立刻退出,然后父进程进程等待,立即成功。随后孙子进程变成孤儿进程,由系统领养,与父进程不再具有关系。也能完成我们的目标。
- 但是这样写不太好,因为创建进程代价很大。
- 创建进程代价很大,很浪费时间,可能让客户很不爽。
- 进程资源有限,如果客户较多,那么没有足够多的进程用来通信。
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9
10 #define BACKLOG 10
11
12 class TcpServer{
13 private:
14 int port;
15 int sockfd;
16 public:
17 TcpServer(int _port)
18 : port(_port)
19 , sockfd(-1)
20 {}
21
22 void InitServer(){
23 sockfd = socket(AF_INET, SOCK_STREAM, 0);
24 //bind
25 struct sockaddr_in local;
26 local.sin_family = AF_INET;
27 local.sin_port = htons(port);
28 local.sin_addr.s_addr = INADDR_ANY;
29 if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0){
30 std::cerr << "bind error" << std::endl;
31 exit(1);
32 }
33 //listen
34 if(listen(sockfd, BACKLOG) < 0){
35 std::cerr << "listen error" << std::endl;
36 exit(2);
37 }
38 }
39
40 void Start(){
41 struct sockaddr_in end_point;
42 socklen_t len = sizeof(end_point);
43 for(;;){
44 // listen successfully, accept
45 int acc_sock = accept(sockfd, (struct sockaddr*)&end_point, &len);
46 if(acc_sock < 0){
47 std::cerr << "accept error" << std::endl;
48 continue;
49 }
50 // 连接建立完成, 开始通信
51 std::cout << "get a new link... " << std::endl;
52
53 //细节,防止新线程没被创建出来,acc_sock被覆盖。
54 int* p = new int(acc_sock);
55 pthread_t tid;
56 //int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
57 // void *(*start_routine) (void *), void *arg);
58 pthread_create(&tid, nullptr, start_routine, (void*)p);
59 }
60 }
61 ~TcpServer(){
62 close(sockfd);
63 }
64
65 private:
66 static void* start_routine(void* arg){
67 pthread_detach(pthread_self());
68 int* p = static_cast(arg);
69 Service(*p);
70 return nullptr;
71 }
72 static void Service(int sockfd){
73 char buf[64] = {0};
74 for(;;){
75 //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
76 ssize_t s = recv(sockfd, buf, sizeof(buf)-1, 0);
77 if(s > 0){
78 buf[s] = 0;
79 std::cout << buf << std::endl;
80
81 //int send(int s, const void *msg, size_t len, int flags);
82 std::string str = buf;
83 // str += "[server] ";
84 send(sockfd, str.c_str(), str.size(), 0);
85 }
86 else if(s == 0){
87 std::cout << "client quit..." << std::endl;
88 break;
89 }
90 else{
91 std::cerr << "recv error" << std::cout;
92 break;
93 }
94 }
95 }
96 };
- 多线程版本也有很多细节。
- 第一个就是我们说过的start_routine作为类内函数,必须是static,不然第一个参数是this指针。
- 其次pthread_create的最后一个参数是否传入this呢?如果不传入this指针,那么我们无法使用类内部函数。如果传入this指针,那么,没有了acc_sock,我们无法实现通信。怎么办呢?我们发现Service函数没有使用到this指针,所以我们可以将Service函数也变成static,然后在start_routine种传入acc_sock。
- 线程是公用文件描述符数组的,所以多线程能连接的客户端也是有限的。
- 有一个很隐晦的bug,当新线程还没有被创建出来的时候,主线程又重新进行线程创建,这就导致sock被覆盖。
- 我们可以这样 int* p = new(sock);
- 这样会拷贝一个p的复制p1,即使p被覆盖,p1也是指向sock。
多进程和多线程的代码都有一个问题:
- 当客户端申请连接的时候,服务器才会给它创建进程/线程,这会让用户很不爽,所以有没有一种方法,在用户没有申请之前就创建好进程/线程,等用户一申请就立刻去执行任务呢?
- 有的,这就是池化技术。



