连接
TCP/IP协议规定网络数据传输应采用大端字节序
- socket地址
struct sockaddr{
unsigned short sa_family;
char sa_data[14];
};
一般不采用上述socket地址,系统兼容性考虑采用sockaddr_in。
#include <netinet/in.h>
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in {
sa_family_t sin_family; //short
in_port_t sin_port; // unsigned short
struct in_addr sin_addr;
};
- socket
sys/socket.h
int socket(int domain, int type, int protocal);
family: AF_INET, AT_INET6, AF_UNIX
AF_UNIX只能用于单一的(unix)进程间通信
AF_INET是针对Internet的,允许在远程主机之间通信
type: SOCK_STREAM(tcp), SOCK_DGRAM(udp)
protocal:协议 0
return:失败-1,成功fd。
- bind
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
INADDR_ANY标识可以和任何的主机通信。
return: 失败-1(设置errno),成功0
- listen
int listen(int sockfd, int backlog);
backlog:最多允许有多上个客户端处于连接等待状态,超过忽略。
return:失败-1,成功0
listen完成后,内核自动完成三次握手,与accept无关。
当有客户端发起链接时,服务器端调用accept()返回并接受这个连接。如果有大量客户端发起连接,服务器来不及处理,尚未accept的客户端就处于连接等待状态。
- accept
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
cliaddr:客户端地址(传出);
addrlen:传入传出(缓冲区大小,地址大小)
return:失败-1,成功:新的文件描述符。
服务器端调用accept()还没有客户端连接请求时,阻塞等待。
链接关闭时,返回错误码ECONNABORTED,此时应重试连接。
perror("accept()"); if((errno == ECONNABORTED) || (errno == EINTR)) continue;
- connect
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
servaddr:服务器地址。
return:失败-1;成功0。
客户端port由内核自动分配。
当连接不成功时,应阻塞等待。
出错原因:
- 指定IP无效。
- 无服务器进程。
- 超时Timeout。
慢系统调用accept,read,write被信号中断时应该重试。对于accept,如果errno为ECONNABORTED,也应该重试。
读写数据
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
return:成功,返回读取到的字节数;失败-1。
有数据到达,返回读到的字节数;无数据到达,
- 连接关闭,返回0
- 连接未关闭,阻塞。
sszie_t write(int fd, const void *buf, size_t count);
return:成功,返回写入的字节数;失败-1。
连接关闭,收到SIGPIPE,返回-1。连接未关闭,
- 发生流量控制,阻塞;
- 无流量控制,返回写入字节数。
关闭socket
关闭socket有两个函数close和shutdown。
int shutdown(int sockfd, int howto);
TCP连接是双向的(可读可写),当我们使用close时,会把读写通道都关闭,有时候希望只关闭一个方向,这个时候用shutdown。
howto=0关闭读通道,可以写。
howto=1关别写通道,可以读。
howto=2关闭读写通道,和close一样。
注:shutdown不能代替close,其仅关闭连接,不销毁连接套接字,其后还要close。
多进程程序里,如果有几个进程共享一个socket,如果使用shutdown,那么所有子进程都不能操作了,这个时候只能使用close来关闭子进程的套接子描述符。
总结
总的来说,网络程序是由两个部分组成的--客户端和服务器端。它们建立的步骤一般是:
服务器端
socket-> bind -> listen -> accept
客户端
socket -> connect
注:由于客户端不需要固定端口,因此不必调用bind()。不调用bind(),端口号由内核自动分配。
客户端不是不可以调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),由内核分配端口号,每次重启服务器时端口号都不一样,客户端连接服务器就会遇到麻烦。
示例
客户端程序读数据,服务器端多进程并发写数据。
每次accept一个新客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。
// ser.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <errno.h> #define SERV_PORT 10000 #define MAX_CONN 2 #define STR_SND "I love you!" int main(int argc, char *argv[]) { int fd, sfd; int ret = 0; struct sockaddr_in saddr, cliaddr; socklen_t slen; fd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == fd){ fprintf(stderr, "socket error\n"); exit(EXIT_FAILURE); } memset(&saddr, 0, sizeof(struct sockaddr_in)); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = htonl(INADDR_ANY); saddr.sin_port = htons(SERV_PORT); ret = bind(fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr)); if(-1 == ret){ fprintf(stderr, "bind error\n"); exit(EXIT_FAILURE); } ret = listen(fd, MAX_CONN); if( -1 == ret){ fprintf(stderr, "listen error\n"); exit(EXIT_FAILURE); } while(1){ slen = sizeof(struct sockaddr); sfd = accept(fd, (struct sockaddr*)&cliaddr, &slen); if(sfd < 0){ // fprintf(stderr, "accept error\n");
perror("accept()");
if((errno == ECONNABORTED) || (errno == EINTR))
continue; } signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN); ret = fork(); if(ret > 0){ printf("connet:%s %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); while(1){ ret = write(sfd, STR_SND, sizeof(STR_SND)); //ret = write(sfd, STR_SND, 100); if(ret == -1){ if(errno == EPIPE){ close(sfd); printf("client [%s:%d] disconnect\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); exit(EXIT_SUCCESS); } } sleep(3); } } else if(ret < 0) { perror("fork"); fprintf(stderr, "fork error\n"); } } close(fd); return 0; }
// cli.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define SERV_IP "127.0.0.1" #define SERV_PORT 10000 #define BSIZE 1024 int main(int argc, char *argv[]) { int fd; int ret = 0; struct sockaddr_in saddr; char buf[BSIZE]; fd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == fd){ fprintf(stderr, "socket error\n"); exit(EXIT_FAILURE); } memset(&saddr, 0, sizeof(struct sockaddr_in)); saddr.sin_family = AF_INET; inet_pton(AF_INET, SERV_IP, &saddr.sin_addr.s_addr); saddr.sin_port = htons(SERV_PORT); ret = connect(fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr)); if( -1 == ret){ fprintf(stderr, "connect error\n"); exit(EXIT_FAILURE); } while(1){ memset(buf, 0, sizeof(buf)); ret = read(fd, buf, sizeof(buf)); if( ret > 0){ if(buf[ret-1] != '\n'){ buf[ret-1] = '\n'; } fprintf(stdout, "%d:%s", ret, buf); fflush(stdout); } else if(ret == 0){ // network disconneted fprintf(stderr, "disconnect \n"); break; } else { if(errno == EINTR){ continue; } } } close(fd); return 0; }
运行结果:
~$gcc ser.c -Wall -o ser ~$gcc cli.c -Wall -o cli ~$./ser connet:127.0.0.1 43731 connet:127.0.0.1 43732 connet:127.0.0.1 43733 connet:127.0.0.1 43734 connet:127.0.0.1 43735 connet:127.0.0.1 43736 client [127.0.0.1:43736] disconnect client [127.0.0.1:43733] disconnect client [127.0.0.1:43734] disconnect client [127.0.0.1:43732] disconnect client [127.0.0.1:43735] disconnect client [127.0.0.1:43731] disconnect ~$./ser bind error ~$ps aux | grep -w 'ser' yuxi 31890 0.0 0.0 2028 60 pts/1 S 23:22 0:00 ./ser yuxi 31894 0.0 0.0 6156 876 pts/1 S+ 23:22 0:00 grep --color=auto -w ser ~$killall ser ~$ps aux | grep -w 'ser' yuxi 31897 0.0 0.0 6156 872 pts/1 S+ 23:22 0:00 grep --color=auto -w ser
"bind error"是因为父进程一直在监听,没有退出。
从上述示例也可以看出,判断网络断开的方法:read返回sockfd,返回0;write返回-1,errno为EPIPE。
注:SIGPIPE默认动作为程序退出,一旦收到SIGPIPE信号程序就退出,从而无法判断errno,故设置signal(SIGPIPE, SIG_IGN);
可参考: