最近看了看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多路復用的小文章。同時,上面的代碼還存在一個問題,那就是服務端只能一個一個得為客戶端服務,也就是所謂的同步。
以上。