最近看了看socket网络编程,对于我这种一点经验都没有的选手来说只能理解一点点吧。所以在此记录一下最近的收获。
socket编程无非就那几个函数,对于服务端来说,主要的为socket(),bind(),listen(),accept(),close()。对于客户端来说主要为connect(),close()等。当然,我所说的只是针对tcp协议而言的。对于udp而言,就可以简单很多,服务端和客户端都建立socket并进行绑定,从而用sendto()和recvfrom()通信即可。
以下直接上一个关于tcp协议的客户端和服务端的程序。
//此为服务端的程序#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <errno.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> #include <netdb.h> //当SIGCHLD信号出现时则执行此函数 //当子进程停止时,SIGCHLD信号会送给父进程,默认是忽略该信号 void sig_handler(int signo) { pid_t pid; int stat; //用WNOHANG参数如果没有任何已终止的进程,它仍会立即返回,而不是像wait那样永远等下去 //waitpid如果成功的话返回子进程的pid,如果没有子进程退出则返回0 pid = waitpid(-1, &stat, WNOHANG); while(pid > 0) { printf("child process terminated (PID: %ld)\n", (long)getpid()); pid = waitpid(-1, &stat, WNOHANG); } return 0; } int main(int argc, char *argv[]) { int listen_fd; int com_fd; int ret; int i; static char recv_buffer[1024]; int len; int port; pid_t pid; /* struct sockaddr { unsigned short sa_family; //address family, AF_xxx char sa_data[14]; //14 bytes of protocal address }; struct sockaddr_in { short int sin_family; //address family, AF_INET unsigned short int sin_port; //port number struct in_addr sin_addr; //internet address unsigned char sin_zero[8]; //same sizeas struct sockaddr }; struct in_addr { uint32_t s_addr; //4 bytes }; sockaddr是通用套接字地址,sockaddr_in是internet环境下套接字的地址形式,两者 结构一样,都为16个字节。 */ struct sockaddr_in clt_addr; struct sockaddr_in srv_addr; //服务器运行时要给出端口信息,该端口为监听端口 if(argc != 2) { printf("Usage: %s port\n", argv[0]); return 1; } //atoi为ascii to integer,字符串转换为整型 port = atoi(argv[1]); //设置处理信号函数 if(signal(SIGCHLD, sig_handler) < 0) { perror("cannot set the signal"); return 1; } //创建套接字用于服务器的监听 if((listen_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("cannot creat listening socket"); return 1; } //将srv_addr全部置零,INADDR_ANY就是inet_addr("0.0.0.0"),表示不确定 //地址,也就是表示本机的所有IP memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); srv_addr.sin_port = htons(port); if((ret = bind(listen_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr))) == -1) { perror("cannot bind server socket"); close(listen_fd); return 1; } //指定监听端口,连接5个客户端 if((ret = listen(listen_fd, 5)) == -1) { perror("cannot listen the client connect request"); close(listen_fd); return 1; } //对每个连接来的客户端创建一个进程,单独与其进行通信 //首先调用read函数读取客户端发送来的信息 //将其转换成为大写后发送回客户端 //当输入@时,程序退出 //用fork()函数的原因是保持服务器能容许多个客户端的连接 while(1) { len = sizeof(clt_addr); if((com_fd = accept(listen_fd, (struct sockaddr*)&clt_addr, &len)) < 0) { //EINTR表示系统调用被信号中断 if(errno == EINTR) { continue; } else { perror("cannot accept client connect request"); close(listen_fd); return 1; } } pid = fork(); if(pid < 0) { perror("cannot create the child process"); close(listen_fd); return 1; } //当有客户端连接的时候,在子进程中进行客户端与服务端的通信 else if(pid == 0) { while((len = read(com_fd, recv_buffer, 1024)) > 0) { printf("Message from client(%d): %s\n", len, recv_buffer); if(recv_buffer[0] == ‘@‘) { break; } for(i = 0; i < len; i++) { recv_buffer[i] = toupper(recv_buffer[i]); } write(com_fd, recv_buffer,len); memset(recv_buffer, 0, 1024); } close(com_fd); return 0; } //父进程直接结束此次循环等待下一个客户端进行连接 else { close(com_fd); } } return 0; }
//此为服务端的程序#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <netdb.h> #include <unistd.h> int main(int argc, char *argv[]) { int connect_fd; int ret; char snd_buffer[1024]; int i; int port; int len; static struct sockaddr_in srv_addr; if(argc != 3) { printf("Usage: %s server_ip_address port\n", argv[1]); return 1; } port = atoi(argv[2]); if((connect_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("cannot creat communication socket"); return 1; } memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_addr.s_addr = inet_addr(argv[1]); srv_addr.sin_port = htons(port); if((ret = connect(connect_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr))) == -1) { perror("cannot connect to the server"); close(connect_fd); return 1; } //memset(snd_buffer, 0, 1024); while(1) { memset(snd_buffer, 0, 1024); write(STDOUT_FILENO, "input message: ", 15); len = read(STDIN_FILENO, snd_buffer, 1024); if(len > 0) write(connect_fd, snd_buffer, len); len = read(connect_fd, snd_buffer, len); if(len > 0) printf("Message from serverL %s\n", snd_buffer); if(snd_buffer[0] == ‘@‘) break; } close(connect_fd); return 0; }
当然,这属于老版本的socket程序,最新的socket编程推荐使用getaddrinfo()函数,这个函数是通过一个hints参数来当做地址的返回标准的。具体的getaddrinfo()函数如下:
struct addrinfo { int ai_flags; //input flags int ai_family; //address family int ai_socktype; //SOCK_STREAM, SOCK_DGRAM int ai_protocol; //socket protocol size_t ai_addrlen; //size of structure pointed to by ai_addr char *ai_cannonname; //canonical name of host struct sockaddr *ai_addr; //pointer to socket address structure struct addrinfo *ai_next; //next structure in linked list }; #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *node, //host name to connect to or an IP address const char *service, //port number, /etc/services const struct addrinfo *hints, //fill with relevant information, only ai_flags, ai_family, ai_socktype, ai_protocol struct addrinfo **res );
上面的服务端程序是通过fork()函数来实现容许多个客户端进行连接的,下面我要用select()函数来实现多个客户端的连接。当然,也同时用了getaddrinfo()这种新特性。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #define PORT "1234" void *get_in_addr(struct sockaddr *sa) { if(sa->sa_family == AF_INET) { return &(((struct sockaddr_in*)sa)->sin_addr); } return &(((struct sockaddr_in6*)sa)->sin6_addr); } int main(void) { fd_set master; fd_set read_fds; int fdmax; int listener; int newfd; struct sockaddr_storage remoteaddr; socklen_t addrlen; char buf[256]; int len; char remoteIP[INET6_ADDRSTRLEN]; int yes = 1; int i, j, rv; // int client[100], n; struct addrinfo hints, *ai, *p; FD_ZERO(&master); FD_ZERO(&read_fds); memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; if((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) { perror("getaddrinfo"); return 1; } for(p = ai; p != NULL; p = p->ai_next) { listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if(listener < 0) { continue; } setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); if(bind(listener, p->ai_addr, p->ai_addrlen) < 0) { close(listener); continue; } break; } if(p == NULL) { perror("socket and bind"); return 2; } freeaddrinfo(ai); if(listen(listener, 10) == -1) { perror("listen"); return 3; } FD_SET(listener, &master); fdmax = listener; while(1) { printf("HERE!!! BEFORE SELECT\n"); read_fds = master; if(select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); return 4; } printf("HERE!!! AFTER SELECT\n"); for(i = 0; i <= fdmax; i++) { printf("round i is %d\n", i ); if(FD_ISSET(i, &read_fds)) { printf("isset i is %d\n", i ); if(i == listener) { addrlen = sizeof(remoteaddr); newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen); printf("listener is %d, newfd is %d,listener i is %d\n", listener, newfd, i); if(newfd == -1) { perror("accept"); } else { FD_SET(newfd, &master); if(newfd > fdmax) { fdmax = newfd; } printf("selectserver: new connection form %s on socket %d\n", inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET6_ADDRSTRLEN), newfd); } } else { // if(i != newfd) { // printf("you are in other‘s zone\n"); // goto fail; // } newfd = i; //注意这步!!!很重要!!! while(1) { printf("in the newfd round\n"); if((len = read(newfd, buf, 256)) <= 0) { perror("read"); break; } printf("newfd i is %d\n", i); // while((len = read(newfd, buf, 256)) > 0) { printf("Message form client(%d): %s", len, buf); if(buf[0] == ‘@‘) break; for(j = 0; j < len; j++) { buf[j] = toupper(buf[j]); } write(newfd, buf, len); memset(buf, 0, 256); // printf("remove client on fd %d\n", newfd); // close(newfd); // FD_CLR(newfd, &master); } // FD_CLR(newfd, &read_fds); printf("remove client on fd %d\n", newfd); close(newfd); FD_CLR(newfd, &master); } } } } // fail: return 8; }
上面的代码里有我调试的打印信息,这样就可以更清晰得看到select()函数是如何得阻塞,如何得得知文件描述符的变化。当然,这个函数我也感觉没太弄清楚,等弄清楚之后在准备详细得写一篇关于I/O多路复用的小文章。同时,上面的代码还存在一个问题,那就是服务端只能一个一个得为客户端服务,也就是所谓的同步。
以上。
参考:《Linux/UNIX系统编程手册》
《Linux编程技术详解》
《Beej‘s Guide to Network Programming Using Internet Sockets》
http://beej.us/guide/bgnet/output/html/multipage/index.html
时间: 2024-10-11 07:30:08