tcp协议的基本情况在此就不详细介绍,上图是《UNIX网络编程》书里的一张图,此图直观的展现了tcp服务器和客户是如何开始通信、进行通信和结束通信的全过程。
一、套接字地址结构介绍
首先介绍Ipv4套接字地址结构:
struct in_addr { in_addr_t s_addr;//32位 ipv4的地址存放在这 }; struct sockaddr_in { __uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };
sin_len是为增加对OSI协议的支持而随4.3BSD-Reno添加的。
sin_family是一个无符号短整数,使用时可以按照不同的规范选用。
sin_port是端口号。
sin_addr是一个结构体,该结构体内保存了ipv4地址。
sin_zero[8]字段未曾被使用过,不过在填写这种套接字地址结构时,总是把它置为0。
为了让套接字函数的输入适应不同协议族(ipv4、ipv6等),提供了通用套接字。
通用套接字地址结构:
struct sockaddr { __uint8_t sa_len; sa_family_t sa_family; char sa_data[14]; };
在使用bind()、accept()等函数时需要把Ipv4套接字地址结构强制转化成通用套接字然后带入函数。例如:
int bind(int,struct sockaddr *,socklen_t);
二、基本套接字函数
socket() 函数:
socket()函数用于创建套接字,socket()函数成功时返回一个类似于文件描述符的非负整数,我们称之为套接字描述符。
int socket(int family, int type, int protocol);
成功返回非负数描述符,失败返回-1
其中:
family参数指明协议族,ipv4一般用AF_INET。
type参数指明套接字类型,有四种类型分别是字节流套接字、 数据报套接字、有序分组套接字、原始套接字。这里我们用字节流套接字,SOCK_STREAM。对另外几种感兴趣的小伙伴可以自己百度一下。
Protocol参数是协议类型,有tcp传输协议、udp传输协议、sctp传输协议。也可以默认为0,以family和type的组合,系统自己赋值。
connect() 函数:
connect()函数用于通信中的主动方向被动方发起建立连接的请求。对于TCP套接字来说,connect()函数会激发TCP的三次握手。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
成功返回0,失败返回-1
sockfd:套接字描述符,由socket()函数返回的。
addr:指向套接字地址结构的指针。
addrlen:套接字地址结构的大小。
bind() 函数:
bind()函数用于将本地协议地址与socket()函数创建的套接字绑定起来。
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
成功返回0,失败返回-1。
sockfd: 套接字描述符,由前面的socket()函数成功时返回。
myaddr: 指向特定于协议的地址结构的指针。
addrlen: 该地址结构的长度。
listen()函数:
listen()函数由TCP服务器调用,主要有两个作用:
1. 将一个未连接的套接字转换为被动套接字,指示内核应该接 收指向该套接字的连接请求。
2. 指定内核应该为相应的套接字队列的最大连接个数。
int listen(int sockfd, int backlog);
成功返回0,失败返回-1。
sockfd: 套接字描述符,由前面的socket()函数成功时返回。
backlog: 指定内核应该为相应的套接字排队的最大连接个数。
accept()函数:
accept()函数由TCP服务器调用,用于返回下一个已完成连接。如果已完成连接队列为空,则阻塞。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
成功则返回套接字描述符,失败返回-1
sockfd: 内核创建的新的套接字描述符,用于描述与返回的客户端之间的连接
cliaddr: 已连接的对端客户对协议地址。
addrlen: 该地址结构的长度。
fork()函数:
pid_t fork(void);
返回:两次返回在子进程中返回0,在父进程中为子进程的id,若出错则为-1。
生成一个和父进程一样的子进程,代替父进程执行下面的操作,解放父进程。
三、服务器端代码
#include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t pid; int ser_socket; struct sockaddr_in se_addr,cl_addr; int addr_len = sizeof(cl_addr); int client; char buffer[4000],renew[4000]; int datanum; if ((ser_socket = socket(AF_INET,SOCK_STREAM,0))<0) { perror("socket"); return -1; } bzero(&se_addr, sizeof(se_addr)); se_addr.sin_family = AF_INET; se_addr.sin_port = htons(5901); se_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(ser_socket,(struct sockaddr *)&se_addr,sizeof(se_addr))<0) { perror("bind"); return -1; } if(listen(ser_socket, 10)<0) { perror("listen"); return -1; } //等待请求 while (1) { printf("Listening on port :%d\n",5901); client = accept(ser_socket, (struct sockaddr*)&cl_addr, (socklen_t *)&addr_len); if(client<0) { perror("accept"); continue; } printf("\n recv client data:\n"); printf("IP is %s\n",inet_ntoa(cl_addr.sin_addr)); printf("port is %d\n",htons(cl_addr.sin_port)); //进行对话,创建线程 if((pid = fork())==0) { close(ser_socket); while(1) { datanum = recv(client, buffer, 4000, 0);// 收数据 if(datanum< 0) { perror("recv"); continue; } buffer[datanum] = '\0'; printf("%d:say %s\n", htons(cl_addr.sin_port), buffer); printf("you want to say:"); scanf("%s",renew); send(client, renew, strlen(renew), 0); if(strcmp(renew, "quit") == 0) { exit(0);//销毁此线程 break; } } } } return 0; }
四、客户端1代码
#include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(int argc, const char * argv[]) { int client_socket; struct sockaddr_in ser_addr; char sendbuf[400]; char recbuf[400]; int sennum,recnum; if((client_socket= socket(AF_INET, SOCK_STREAM, 0))<0) { perror("socket"); return -1; } ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(5901); ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(client_socket, (struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0) { perror("connect"); return 1; } printf("connect with destination host...\n"); while(1) { printf("Input your world:>"); scanf("%s", sendbuf); printf("\n"); send(client_socket, sendbuf, strlen(sendbuf), 0); recnum = recv(client_socket, recbuf, 4000, 0); recbuf[recnum] = '\0'; printf("recv is: %s\n", recbuf); if(strcmp(recbuf, "quit") == 0) break; } close(client_socket); return 0; }
五、客户端2代码
#include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(int argc, const char * argv[]) { int client_socket; struct sockaddr_in ser_addr; char sendbuf[400]; char recbuf[400]; int sennum,recnum; if((client_socket= socket(AF_INET, SOCK_STREAM, 0))<0) { perror("socket"); return -1; } ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(5901); ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(client_socket, (struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0) { perror("connect"); return 1; } printf("connect with destination host...\n"); while(1) { printf("Input your world:>"); scanf("%s", sendbuf); printf("\n"); send(client_socket, sendbuf, strlen(sendbuf), 0); recnum = recv(client_socket, recbuf, 4000, 0); recbuf[recnum] = '\0'; printf("recv is: %s\n", recbuf); if(strcmp(recbuf, "quit") == 0) break; } close(client_socket); return 0; }
总结:《unix网络编程》不仅仅适用于unix系统的网络编程,也讲述了很多通用性的内容,强力安利一波。自己写的这个简单的多并发服务器是个基于tcp的多并发聊天程序。先运行服务器端,然后再运行客户端,服务器端可以与客户1、2一起聊天,不用等待某一客户的回复。
代码测试环境是mac 10.13.1,本人不才,若文中有错误,恳请大家积极指正。