我正在尝试在C中创建一个多线程服务器- 客户端文件传输系统。有些客户端将发送或列出或做其他选择(在交换机的情况下,您可以看到),而服务器则存储文件并提供大量服务客户。
就我所知,多线程意识形态确实很困难。它需要太多的经验而不是知识。我已经在该项目上工作了一个多星期,但我一直无法解决这些问题。
有4个选择:第一个是在其目录中列出客户端的本地文件,第二个是在客户端和服务器之间传输的列表文件,第三个是从用户读取文件名并将文件复制到服务器的目录中。
我的关键问题是关于多线程。我无法连接多个客户端。我已经读过a到z堆的代码,但是我确实无法捕捉到我的错误而陷入困境。
另一个问题是,客户端将在SIGINT被捕获时结束,但是,例如,在按ctrl- c选择列表文件后,它不会停止。服务器文件也有同样的问题。与客户端捕获相比,这更麻烦,因为当服务器到达时SIGINT,客户端将分别与服务器断开连接。
SIGINT
感谢您的帮助!
服务器
/* Soner Receive a file over a socket. Saves it to output.tmp by default. Interface: ./executable [<port>] Defaults: - output_file: output.tmp - port: 12345 */ #define _XOPEN_SOURCE 700 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <fcntl.h> #include <netdb.h> /* getprotobyname */ #include <netinet/in.h> #include <sys/stat.h> #include <sys/socket.h> #include <unistd.h> #include <pthread.h> pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; enum { PORTSIZE = 5 }; void* forClient(void* ptr); void sig_handler(int signo) { if (signo == SIGINT) printf("!! OUCH, CTRL - C received by server !!\n"); } int main(int argc, char **argv) { struct addrinfo hints, *res; int enable = 1; int filefd; int server_sockfd; unsigned short server_port = 12345u; char portNum[PORTSIZE]; socklen_t client_len[BUFSIZ]; struct sockaddr_in client_address[BUFSIZ]; int client_sockfd[BUFSIZ]; int socket_index = 0; pthread_t threads[BUFSIZ]; if (argc != 2) { fprintf(stderr, "Usage ./server <port>\n"); exit(EXIT_FAILURE); } server_port = strtol(argv[1], NULL, 10); memset(&hints, 0, sizeof hints); hints.ai_family = AF_INET; //ipv4 hints.ai_socktype = SOCK_STREAM; // tcp hints.ai_flags = AI_PASSIVE; // fill in my IP for me sprintf(portNum, "%d", server_port); getaddrinfo(NULL, portNum, &hints, &res); server_sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (server_sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } if (setsockopt(server_sockfd, SOL_SOCKET, (SO_REUSEPORT | SO_REUSEADDR), &enable, sizeof(enable)) < 0) { perror("setsockopt(SO_REUSEADDR) failed"); exit(EXIT_FAILURE); } if (bind(server_sockfd, res->ai_addr, res->ai_addrlen) == -1) { perror("bind"); exit(EXIT_FAILURE); } if (listen(server_sockfd, 5) == -1) { perror("listen"); exit(EXIT_FAILURE); } fprintf(stderr, "listening on port %d\n", server_port); while (1) { client_len[socket_index] = sizeof(client_address[socket_index]); puts("waiting for client"); client_sockfd[socket_index] = accept( server_sockfd, (struct sockaddr*)&client_address[socket_index], &client_len[socket_index] ); if (client_sockfd[socket_index] < 0) { perror("Cannot accept connection\n"); close(server_sockfd); exit(EXIT_FAILURE); } pthread_create( &threads[socket_index], NULL, forClient, (void*)client_sockfd[socket_index]); if(BUFSIZ == socket_index) { socket_index = 0; } else { ++socket_index; } pthread_join(threads[socket_index], NULL); close(filefd); close(client_sockfd[socket_index]); } return EXIT_SUCCESS; } void* forClient(void* ptr) { int connect_socket = (int) ptr; int filefd; ssize_t read_return; char buffer[BUFSIZ]; char *file_path; char receiveFileName[BUFSIZ]; int ret = 1; // Thread number means client's id printf("Thread number %ld\n", pthread_self()); pthread_mutex_lock( &mutex1 ); // until stop receiving go on taking information while (recv(connect_socket, receiveFileName, sizeof(receiveFileName), 0)) { file_path = receiveFileName; fprintf(stderr, "is the file name received? ? => %s\n", file_path); filefd = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (filefd == -1) { perror("open"); exit(EXIT_FAILURE); } do { read_return = read(connect_socket, buffer, BUFSIZ); if (read_return == -1) { perror("read"); exit(EXIT_FAILURE); } if (write(filefd, buffer, read_return) == -1) { perror("write"); exit(EXIT_FAILURE); } } while (read_return > 0); } pthread_mutex_unlock( &mutex1 ); fprintf(stderr, "Client dropped connection\n"); pthread_exit(&ret); }
客户端
/* Soner Send a file over a socket. Interface: ./executable [<sever_hostname> [<port>]] Defaults: - server_hostname: 127.0.0.1 - port: 12345 */ #define _XOPEN_SOURCE 700 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <arpa/inet.h> #include <fcntl.h> #include <netdb.h> /* getprotobyname */ #include <netinet/in.h> #include <sys/stat.h> #include <sys/socket.h> #include <unistd.h> // NOTE/BUG: this didn't provide enough space for a 5 digit port + EOS char #if 0 enum { PORTSIZE = 5 }; #else enum { PORTSIZE = 6 }; #endif void sig_handler(int signo) { if (signo == SIGINT) printf("!! OUCH, CTRL - C received on client !!\n"); } int main(int argc, char **argv) { struct addrinfo hints, *res; char *server_hostname = "127.0.0.1"; char file_path[BUFSIZ]; char *server_reply = NULL; char *user_input = NULL; char buffer[BUFSIZ]; int filefd; int sockfd; ssize_t read_return; struct hostent *hostent; unsigned short server_port = 12345; char portNum[PORTSIZE]; char remote_file[BUFSIZ]; int select; char *client_server_files[BUFSIZ]; int i = 0; int j; // char filename_to_send[BUFSIZ]; if (argc != 3) { fprintf(stderr, "Usage ./client <ip> <port>\n"); exit(EXIT_FAILURE); } server_hostname = argv[1]; server_port = strtol(argv[2], NULL, 10); /* Prepare hint (socket address input). */ hostent = gethostbyname(server_hostname); if (hostent == NULL) { fprintf(stderr, "error: gethostbyname(\"%s\")\n", server_hostname); exit(EXIT_FAILURE); } memset(&hints, 0, sizeof hints); hints.ai_family = AF_INET; // ipv4 hints.ai_socktype = SOCK_STREAM; // tcp hints.ai_flags = AI_PASSIVE; // fill in my IP for me sprintf(portNum, "%d", server_port); getaddrinfo(NULL, portNum, &hints, &res); sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } /* Do the actual connection. */ if (connect(sockfd, res->ai_addr, res->ai_addrlen) == -1) { perror("connect"); return EXIT_FAILURE; } while (1) { if (signal(SIGINT, sig_handler)) { break; } puts("connected to the server"); puts("-----------------"); puts("|1 - listLocal| \n|2 - listServer| \n|3 - sendFile| \n|4 - help| \n|5 - exit| "); puts("-----------------"); while (1) { scanf("%d", &select); switch (select) { case 1: // list files of client's directory system("find . -maxdepth 1 -type f | sort"); break; case 2: // listServer puts("---- Files btw Server and the Client ----"); for (j = 0; j < i; ++j) { puts(client_server_files[j]); } break; case 3: // send file memset(file_path, 0, sizeof file_path); scanf("%s", file_path); memset(remote_file, 0, sizeof remote_file); // send file name to server sprintf(remote_file, "%s", file_path); send(sockfd, remote_file, sizeof(remote_file), 0); filefd = open(file_path, O_RDONLY); if (filefd == -1) { perror("open send file"); //exit(EXIT_FAILURE); break; } while (1) { read_return = read(filefd, buffer, BUFSIZ); if (read_return == 0) break; if (read_return == -1) { perror("read"); //exit(EXIT_FAILURE); break; } if (write(sockfd, buffer, read_return) == -1) { perror("write"); //exit(EXIT_FAILURE); break; } } // add files in char pointer array client_server_files[i++] = file_path; close(filefd); break; case 5: free(user_input); free(server_reply); exit(EXIT_SUCCESS); default: puts("Wrong selection!"); break; } } } free(user_input); free(server_reply); exit(EXIT_SUCCESS); }
我修复了其他人提到的大多数错误。
使多线程/多客户端正常工作的要点:
消除互斥。
将先前索引的所有数组合并socket_index到新的“控件”结构中。主线程为结构执行malloc,将其填充,然后将结构指针传递给线程。
socket_index
pthread_join从主线程中删除并运行所有分离的线程。main不再对客户端线程进行任何关闭/清理。
pthread_join
客户端线程现在执行关闭/清理/释放。
即使有所有这些,服务器/客户端代码仍然需要做一些工作,但是现在,它 确实 可以与多个同时进行的客户端连接一起工作,我认为这是主要问题。
注意:之前 我已经回答了一个类似的问题:使用popen()通过套接字执行命令要特别注意“ flag”字符的讨论。
无论如何,这是代码。我已经清理了它,注释了错误和修复,并用包裹了旧/新代码#if 0。请注意,某些“旧”代码并非纯粹是原始代码,而是我的过渡版本。[请原谅免费样式清理]:
#if 0
server.c:
/* Soner Receive a file over a socket. Saves it to output.tmp by default. Interface: ./executable [<port>] Defaults: - output_file: output.tmp - port: 12345 */ #define _XOPEN_SOURCE 700 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <arpa/inet.h> #include <fcntl.h> #include <netdb.h> /* getprotobyname */ #include <netinet/in.h> #include <sys/stat.h> #include <sys/socket.h> #include <unistd.h> #include <pthread.h> // NOTE: this consolidates four arrays that were indexed by socket_index struct client { socklen_t client_len; struct sockaddr_in client_address; int client_sockfd; pthread_t thread; }; // NOTE: no longer used/needed for true multiclient #if 0 pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; #endif // NOTE/BUG: this didn't provide enough space for a 5 digit port + EOS char #if 0 enum { PORTSIZE = 5 }; #else enum { PORTSIZE = 6 }; #endif void *forClient(void *ptr); void sig_handler(int signo) { if (signo == SIGINT) printf("!! OUCH, CTRL - C received by server !!\n"); } int main(int argc, char **argv) { struct addrinfo hints, *res; int enable = 1; //int filefd; // NOTE: this is never initialized/used int server_sockfd; unsigned short server_port = 12345u; char portNum[PORTSIZE]; // NOTE: now all client related data is malloc'ed #if 0 int socket_index = 0; #else struct client *ctl; #endif if (argc != 2) { fprintf(stderr, "Usage ./server <port>\n"); exit(EXIT_FAILURE); } server_port = strtol(argv[1], NULL, 10); memset(&hints, 0, sizeof hints); hints.ai_family = AF_INET; // ipv4 hints.ai_socktype = SOCK_STREAM; // tcp hints.ai_flags = AI_PASSIVE; // fill in my IP for me sprintf(portNum, "%d", server_port); getaddrinfo(NULL, portNum, &hints, &res); server_sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (server_sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } if (setsockopt(server_sockfd, SOL_SOCKET, (SO_REUSEPORT | SO_REUSEADDR), &enable, sizeof(enable)) < 0) { perror("setsockopt(SO_REUSEADDR) failed"); exit(EXIT_FAILURE); } if (bind(server_sockfd, res->ai_addr, res->ai_addrlen) == -1) { perror("bind"); exit(EXIT_FAILURE); } if (listen(server_sockfd, 5) == -1) { perror("listen"); exit(EXIT_FAILURE); } fprintf(stderr, "listening on port %d\n", server_port); // NOTE: we want the threads to run detached so we don't have to wait // for them to do cleanup -- the thread now does its own close/cleanup pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,1); while (1) { // NOTE/BUG: using a fixed list, if you actually let threads detach, // you don't know which thread completes allowing its control struct // to be reused // the solution is to allocate a fresh one, fill it, pass it to the // thread and let the _thread_ do all the closes and cleanup #if 0 ctl = &control_list[socket_index]; #else ctl = malloc(sizeof(struct client)); if (ctl == NULL) { perror("malloc"); exit(EXIT_FAILURE); } #endif ctl->client_len = sizeof(ctl->client_address); puts("waiting for client"); ctl->client_sockfd = accept(server_sockfd, (struct sockaddr *) &ctl->client_address, &ctl->client_len); if (ctl->client_sockfd < 0) { perror("Cannot accept connection\n"); close(server_sockfd); exit(EXIT_FAILURE); } // NOTE: we're running the threads detached now and we're passing down // extra information just in case the client loop needs it #if 0 pthread_create(&ctl->thread, NULL, forClient, ctl); #else pthread_create(&ctl->thread, &attr, forClient, ctl); #endif #if 0 if (BUFSIZ == socket_index) { socket_index = 0; } else { ++socket_index; } #endif // NOTE/BUG: this is why you couldn't do multiple clients at the same // time -- you are doing a thread join // but you _had_ to because the main thread didn't know when a thread // was done with the control struct without the join #if 0 pthread_join(threads[socket_index], NULL); close(filefd); close(client_sockfd[socket_index]); #endif } return EXIT_SUCCESS; } void * forClient(void *ptr) { #if 0 int connect_socket = (int) ptr; #else struct client *ctl = ptr; int connect_socket = ctl->client_sockfd; #endif int filefd; ssize_t read_return; char buffer[BUFSIZ]; char *file_path; long long file_length; char receiveFileName[BUFSIZ]; //int ret = 1; // Thread number means client's id printf("Thread number %ld\n", pthread_self()); // NOTE: to run parallel threads, this prevents that #if 0 pthread_mutex_lock(&mutex1); #endif // until stop receiving go on taking information while (recv(connect_socket, receiveFileName, sizeof(receiveFileName), 0)) { // NOTE/FIX2: now we have the client send us the file length so we // know when to stop the read loop below file_length = strtoll(receiveFileName,&file_path,10); if (*file_path != ',') { fprintf(stderr,"syntax error in request -- '%s'\n", receiveFileName); exit(EXIT_FAILURE); } file_path += 1; fprintf(stderr, "is the file name received? ? => %s [%lld bytes]\n", file_path,file_length); // NOTE: if you want to see _why_ sending the length is necessary, // uncomment this line and the "unable to send two files" bug will // reappear //file_length = 1LL << 62; filefd = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (filefd == -1) { perror("open"); exit(EXIT_FAILURE); } // NOTE/BUG2/FIX: now we only read up to what we're told to read // previously, we would keep trying to read, so on the _second_ // send, our read call here would get the data that _should_ have // gone into the recv above // in other words, we'd lose synchronization with what the client // was sending us [and we'd put the second filename into the first // file as data at the bottom] for (; file_length > 0; file_length -= read_return) { read_return = BUFSIZ; if (read_return > file_length) read_return = file_length; read_return = read(connect_socket, buffer, read_return); if (read_return == -1) { perror("read"); exit(EXIT_FAILURE); } if (read_return == 0) break; if (write(filefd, buffer, read_return) == -1) { perror("write"); exit(EXIT_FAILURE); } } fprintf(stderr,"file complete\n"); // NOTE/BUG: filefd was never closed #if 1 close(filefd); #endif } #if 0 pthread_mutex_unlock(&mutex1); #endif fprintf(stderr, "Client dropped connection\n"); // NOTE: do all client related cleanup here // previously, the main thread was doing the close, which is why it had // to do the pthread_join close(connect_socket); free(ctl); // NOTE: this needs a void * value like below #if 0 pthread_exit(&ret); #endif return (void *) 0; }
client.c:
/* Soner Send a file over a socket. Interface: ./executable [<sever_hostname> [<port>]] Defaults: - server_hostname: 127.0.0.1 - port: 12345 */ #define _XOPEN_SOURCE 700 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <arpa/inet.h> #include <fcntl.h> #include <netdb.h> /* getprotobyname */ #include <netinet/in.h> #include <sys/stat.h> #include <sys/socket.h> #include <unistd.h> // NOTE/BUG: this didn't provide enough space for a 5 digit port + EOS char #if 0 enum { PORTSIZE = 5 }; #else enum { PORTSIZE = 6 }; #endif // NOTE2: the "volatile" attribute here is critical to proper operation volatile int signo_taken; // NOTE/BUG2: don't use BUFSIZ when you really want something else #define MAXFILES 1000 void sig_handler(int signo) { // NOTE/BUG2/FIX: doing printf within a signal handler is _not_ [AFAIK] a // safe thing to do because it can foul up the internal structure data of // stdout if the base task was doing printf/puts and the signal occurred // in the middle -- there are a number of other restrictions, such as // _no_ malloc, etc. // so, just alert the base layer and let it handle things when it's in a // "safe" state to do so ... signo_taken = signo; } int main(int argc, char **argv) { struct addrinfo hints, *res; char *server_hostname = "127.0.0.1"; char file_path[BUFSIZ]; char *server_reply = NULL; char *user_input = NULL; char buffer[BUFSIZ]; int filefd; int sockfd; struct stat st; ssize_t read_return; struct hostent *hostent; unsigned short server_port = 12345; char portNum[PORTSIZE]; char remote_file[BUFSIZ]; int select; char *client_server_files[MAXFILES]; int i = 0; int j; // char filename_to_send[BUFSIZ]; if (argc != 3) { fprintf(stderr, "Usage ./client <ip> <port>\n"); exit(EXIT_FAILURE); } server_hostname = argv[1]; server_port = strtol(argv[2], NULL, 10); /* Prepare hint (socket address input). */ hostent = gethostbyname(server_hostname); if (hostent == NULL) { fprintf(stderr, "error: gethostbyname(\"%s\")\n", server_hostname); exit(EXIT_FAILURE); } memset(&hints, 0, sizeof hints); hints.ai_family = AF_INET; // ipv4 hints.ai_socktype = SOCK_STREAM; // tcp hints.ai_flags = AI_PASSIVE; // fill in my IP for me sprintf(portNum, "%d", server_port); getaddrinfo(NULL, portNum, &hints, &res); sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } /* Do the actual connection. */ if (connect(sockfd, res->ai_addr, res->ai_addrlen) == -1) { perror("connect"); return EXIT_FAILURE; } // NOTE/FIX2: this only needs to be done once, since the desired action is // to [cleanly] stop the program signal(SIGINT, sig_handler); // NOTES: // (1) instead of using signo_taken as is done, below there are alternate // ways to handle signals with sigsetjmp and siglongjmp // (2) but the main reason to _not_ do this is to prevent the handler // from messing up a file transfer while (! signo_taken) { puts("connected to the server"); #if 0 puts("-----------------"); puts("|1 - listLocal| \n|2 - listServer| \n|3 - sendFile| \n|4 - help| \n|5 - exit| "); puts("-----------------"); #endif while (! signo_taken) { // NOTE: not a bug, but it helps the user to output the menu each // time #if 1 puts("-----------------"); puts("|1 - listLocal| \n|2 - listServer| \n|3 - sendFile| \n|4 - help| \n|5 - exit| "); puts("-----------------"); #endif scanf("%d", &select); // NOTE: we should check this after _any_ call that requests user // input (e.g. scanf, fgets(...,stdin), etc.) if (signo_taken) break; switch (select) { case 1: // list files of client's directory system("find . -maxdepth 1 -type f | sort"); break; case 2: // listServer puts("---- Files btw Server and the Client ----"); for (j = 0; j < i; ++j) { puts(client_server_files[j]); } break; case 3: // send file fputs("Enter filename: ",stdout); fflush(stdout); memset(file_path, 0, sizeof file_path); scanf("%s", file_path); if (signo_taken) break; // NOTE/FIX: check the file _before_ sending request to server // and we [now] want to know the file length so we can send // that to the server so it will know when to stop receiving #if 1 filefd = open(file_path, O_RDONLY); if (filefd == -1) { perror("open send file"); // exit(EXIT_FAILURE); break; } // get the file's byte length if (fstat(filefd,&st) < 0) { perror("stat send file"); // exit(EXIT_FAILURE); close(filefd); break; } #endif // send file name to server memset(remote_file, 0, sizeof(remote_file)); #if 0 sprintf(remote_file, "%s", file_path); #else sprintf(remote_file, "%lld,%s", (long long) st.st_size,file_path); #endif send(sockfd, remote_file, sizeof(remote_file), 0); // NOTE/BUG2: this should be done above to _not_ confuse server #if 0 filefd = open(file_path, O_RDONLY); if (filefd == -1) { perror("open send file"); // exit(EXIT_FAILURE); break; } #endif while (1) { read_return = read(filefd, buffer, BUFSIZ); if (read_return == 0) break; if (read_return == -1) { perror("read"); // exit(EXIT_FAILURE); break; } if (write(sockfd, buffer, read_return) == -1) { perror("write"); // exit(EXIT_FAILURE); break; } } close(filefd); // add files in char pointer array // NOTE/BUG2: file_path gets overwritten, so we must save it // here #if 0 client_server_files[i++] = file_path; #else if (i < MAXFILES) client_server_files[i++] = strdup(file_path); #endif puts("file complete"); break; case 5: free(user_input); free(server_reply); exit(EXIT_SUCCESS); break; default: puts("Wrong selection!"); break; } } } // NOTE/FIX2: we output this here when it's save to do so if (signo_taken) printf("!! OUCH, CTRL - C received on client !!\n"); free(user_input); free(server_reply); exit(EXIT_SUCCESS); }
更新:
我已经解决了连接中断问题,但是仍然出现信号。我在文件发送和信号处理上又留下了两个问题
我已经对客户端信号处理进行了重新设计,以使其按预期方式工作(即打印消息并停止客户端)。
我还解决了只能发送一个文件的问题。要了解这一点,请考虑客户端和服务器的操作。
To send a file, client prompts for filename, does a send call with the filename in it. It then opens the file and does a read/write loop to send the file data to the server [and then closes the file descriptor].
send
To receive a file, server does a recv call to get the filename. It then opens the file [for output] and does a read/write to write the data from the socket to the file [and then closes the file descriptor].
recv
Here is the problem: The termination condition for the server’s read/write loop is to wait until the read(connect_socket,...) call returns 0. But, it will not return zero [unless the socket has been closed].
read(connect_socket,...)
So, now the client does a send call to send the second filename. But, the data for this, instead of going into the server’s recv call, will merely be part of the read buffer. That is, the second filename will just be appended to the first file as data.
read
The solution is to have the client tell the server what the file size is. So, instead of the client doing a send of filename, it now does a send of filesize,filename
filename
filesize,filename
The server will now decode this filesize and split off the filename in the recv buffer. Now, the server’s read/write loop will maintain a count of how many bytes still need to be read and the loop stops when the remaining count hits zero.
There were one or two other minor bugs. I’ve updated both client.c and server.c with the bug fixes and annotations