服务器需要绑定端口号,但是客户端不需要绑定,这是为什么?
- 客户端不需要绑定端口号和ip,但是客户端也有自己的端口号和ip。
- 一台电脑有很多个客户端,如果你想要客户端强行绑定端口号,那么就需要所有的公司进行协商,每个客户端使用不同的端口号。但这是不可能的。如果你强行让客户端绑定端口号,那么就极有可能引起冲突,使得某些客户端启动失败。
- 但是服务器不一样,因为服务器一般只有一个,而且服务器一般是一个公司内部的东西,可以协商。而且服务器的端口号和ip地址必须是确定的,众所周知的,因为一台服务器连接着很多客户端,否则就可能找不到服务器。
- 客户端也需要唯一性,但是不要求确定性。我们可以让操作系统来帮助我们分配端口号。因为端口号资源也有上限(16位),操作系统需要管理端口号。所以哪些端口号没有被使用,只有操作系统知道。
- 客户端也有ip地址和端口号,在recv和send的时候,操作系统会帮助我们自动绑定。
INADDR_ANY
-
绑定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 #include <iostream>
2 #include <cstdlib>
3 #include <sys/types.h>
4 #include <string>
5 #include <sys/socket.h>
6 #include <unistd.h>
7 #include <arpa/inet.h>
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
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
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
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
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
69 std::string str = buf;
70
71 send(sockfd, str.c_str(), str.size(), 0);
72 }
73 }
74 }
75 };
1 #pragma once
2 #include <iostream>
3 #include <cstdlib>
4 #include <sys/types.h>
5 #include <string>
6 #include <cstring>
7 #include <sys/socket.h>
8 #include <unistd.h>
9 #include <netinet/in.h>
10 #include <arpa/inet.h>
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
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套接字。这种写法有很多问题。
问题1
我们发现tcp客户端断开之后,重新连接不上。
ctrl + z
bg 任务号
jobs
问题2
我们发现当有多个客户端想连接服务器时,只有第一个服务器可以正常使用。
-
这是因为单进程的服务器会导致所有任务共用一份资源,如果一个任务卡死,就会导致所有任务卡死。 -
所以第一个客户端将服务器卡在了Service的死循环中,导致其他的客户端无法连上服务器。 -
我们使用多进程来编写服务器,这样父进程主要用来接收连接,然后fork子进程去完成通信。 -
基于以上,我们的多进程版本的tcp服务器如下:
1 #include <iostream>
2 #include <cstdlib>
3 #include <sys/types.h>
4 #include <string>
5 #include <sys/socket.h>
6 #include <unistd.h>
7 #include <arpa/inet.h>
8 #include <signal.h>
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
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
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
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);
61 }
62 }
63 ~TcpServer(){
64 close(sockfd);
65 }
66
67 private:
68 void Service(int sockfd){
69 char buf[64] = {0};
70 for(;;){
71
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
78 std::string str = buf;
79
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 <iostream>
2 #include <cstdlib>
3 #include <sys/types.h>
4 #include <string>
5 #include <sys/socket.h>
6 #include <unistd.h>
7 #include <arpa/inet.h>
8 #include <pthread.h>
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
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
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
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
54 int* p = new int(acc_sock);
55 pthread_t tid;
56
57
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<int*>(arg);
69 Service(*p);
70 return nullptr;
71 }
72 static void Service(int sockfd){
73 char buf[64] = {0};
74 for(;;){
75
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
82 std::string str = buf;
83
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。
线程池版本的多线程tcp
多进程和多线程的代码都有一个问题:
- 当客户端申请连接的时候,服务器才会给它创建进程/线程,这会让用户很不爽,所以有没有一种方法,在用户没有申请之前就创建好进程/线程,等用户一申请就立刻去执行任务呢?
- 有的,这就是池化技术。
|