前面介绍了Socket 编程函数 在编写 Socket 程序之前还需要了解TCP连接是如何建立的。(参考资料《Unix 网络编程》)
建立一个 TCP 连接时会发生下述情形:
1. 服务器必须准备好接受外来的连接。这通常通过调用 socket、bind 和 listen 这三个函数来完成,我们称之为被动打开。
2. 客户通过调用 connect 发起主动打开,这导致客户TCP发送一个SYN(同步)分节,标识希望连接的服务器端口以及初始序号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。
3. 服务器发送回一个包含服务器初始序号以及对客户端 SYN 段确认的 SYN + ACK 段作为应答,由于一个 SYN 占用一个序号,因此确认序号设置为客户端初始序号加 1。
4. 客户端发送确认序号为服务器初始序号加 1 的 ACK 段,对服务器 SYN 段进行确认。
这种交换至少需要三个分组,因此称之为TCP的三路握手。
一旦TCP建立连接,客户/服务器之间便可以进行数据通信。
1. 服务器端
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/socket.h> #include<sys/types.h> #include<unistd.h> #include<netinet/in.h> #include <errno.h> #define PORT 6666 int main(int argc,char **argv) { int ser_sockfd,cli_sockfd; int err,n; int addlen; struct sockaddr_in ser_addr; struct sockaddr_in cli_addr; char recvline[200],sendline[200]; ser_sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字 if(ser_sockfd == -1) { printf("socket error:%s\n",strerror(errno)); return -1; } bzero(&ser_addr,sizeof(ser_addr)); /*在待捆绑到该TCP套接口(sockfd)的网际套接口地址结构中填入通配地址(INADDR_ANY) 和服务器的众所周知端口(PORT,这里为6666),这里捆绑通配地址是在告知系统:要是系统是 多宿主机(具有多个网络连接的主机),我们将接受宿地址为任何本地接口的地址*/ ser_addr.sin_family = AF_INET; ser_addr.sin_addr.s_addr = htonl(INADDR_ANY); ser_addr.sin_port = htons(PORT); //将网际套接口地址结构捆绑到该套接口 err = bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)); if(err == -1) { printf("bind error:%s\n",strerror(errno)); return -1; } //将套接口转换为一个监听套接口,监听等待来自客户端的连接请求 err = listen(ser_sockfd,5); if(err == -1) { printf("listen error\n"); return -1; } printf("listen the port:\n"); while(1) { addlen = sizeof(struct sockaddr); //等待阻塞,等待客户端申请,并接受客户端的连接请求 //accept成功,将创建一个新的套接字,并为这个新的套接字分配一个套接字描述符 cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr,&addlen); if(cli_sockfd == -1) { printf("accept error\n"); } //数据传输 while(1) { printf("waiting for client...\n"); n = recv(cli_sockfd,recvline,1024,0); if(n == -1) { printf("recv error\n"); } recvline[n] = '\0'; printf("recv data is:%s\n",recvline); printf("Input your words:"); scanf("%s",sendline); send(cli_sockfd,sendline,strlen(sendline),0); } close(cli_sockfd); } close(ser_sockfd); return 0; }
1.首先通过 socket 函数创建套接字,此时套接字数据结构字段并未填充,在使用之前必须调用过程来填充对应字段,这里在地址结构中填入通配地址(INADDR_ANY),通配地址就是指定地址为 0.0.0.0 的地址,表示服务器接受机器上所有IP地址的连接,用于多IP机器上。这样无论客户 connect 哪个IP地址,服务器端都会接收到请求,即接受宿地址为任何本地接口的地址。如果是指定地址,那么机器只有 connect 这个地址才能成功。后面是填充端口号,如果指定为 0,则由系统随机选择一个未被使用的端口。
2. bind 将没有指定端口的 socket(ser_sockfd)绑定到我们指定的端口上(通配地址+指定端口号),服务器是通过它们的众所周知端口被大家认识的。这样 socket(ser_sockfd)就与指定的端口产生了关联,即指向了指定端口。
3. listen 将套接口转换为一个监听套接口,被动打开,允许监听客户端的连接请求,然后 accept 客户端的连接请求,没有请求则阻塞。
5. accept 成功后,将创建新的套接字,并为新套接字分配一个套接口描述符,该套接字除了记录本地(服务器)的IP和端口号信息外,还记录了目的(客户)IP和端口号信息。服务器与客户的通信则是通过该新创建的套接字(已连接套接字)进行。
2. 客户端
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/socket.h> #include<sys/types.h> #include<unistd.h> #include<netinet/in.h> #define PORT 6666 int main(int argc,char **argv) { int sockfd; int err,n; struct sockaddr_in addr_ser; char sendline[200],recvline[200]; sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字 if(sockfd == -1) { printf("socket error\n"); return -1; } bzero(&addr_ser,sizeof(addr_ser)); /*用通配地址和指定端口号装填一个网际接口地址结构*/ addr_ser.sin_family = AF_INET; addr_ser.sin_addr.s_addr = htonl(INADDR_ANY); addr_ser.sin_port = htons(PORT); //TCP:客户(sockfd)向服务器(套接口地址结构)发起连接,主动请求 //服务器的IP地址和端口号有参数addr_ser指定 err = connect(sockfd,(struct sockaddr *)&addr_ser,sizeof(addr_ser)); if(err == -1) { printf("connect error\n"); return -1; } printf("connect with server...\n"); //数据传输 while(1) { printf("Input your words:"); scanf("%s",sendline); send(sockfd,sendline,strlen(sendline),0); printf("waiting for server...\n"); n = recv(sockfd,recvline,100,0); recvline[n] = '\0'; printf("recv data is:%s\n",recvline); } return 0; }1. 客户端同样通过 socket 创建套接字,TCP 客户通常不把IP地址捆绑到它的套接口上,当连接套接口时,内核将根据所用外出网络接口来确定源IP地址,并选择一个临时端口作为源端口。
3. 客户端向服务器发起连接请求,connect 成功后,其请求连接的服务器的IP和端口号信息将会写入该套接字,这样该套接字也同时记录了本地和目的的IP地址和端口信息。也就可以进行通信了。
结果如下:
总的说来:
对于服务器程序而言,编程步骤如下:
调用socket,创建一个不与任何网络连接相对应的套接口(socket),其返回值是一个文件描述符;调用bind,将刚刚创建的 socket 与本机的 IP地址和端口号绑定,这样一来 socket 就变成了一个 3元组;调用 listen,将 socket 指定为一个被动套接口(又称监听套接口),专门用于监听客户端到达的网络连接请求;调用 accept,使 socket 进入到可以接收网络连接请求的状态,此时服务器将能够接收客户端发起的连接,但如果没有客户端发起连接,那么服务器将进入阻塞态,直到有客户端连接到达,accept 将返回;此时,accept 的返回值是另一个 socket(文件描述符),这个 socket 被称为连接套接口,此套接口是一个5元组(其中远端IP和远端端口号来自于客户机发起连接时发送到服务器的数据包,其实就是 3次握手的第一个数据包);调用 read,从连接套接口接收客户端发送过来的数据,如果此时客户端并未发送数据,那么 read将阻塞到有数据到达为止;根据客户端的要求进行数据处理,调用 write,向连接套接口写入处理后的数据,该数据将通过网络达到客户端;当所有数据都已处理完毕,将调用 close 关闭连接套接口,这将在物理上激发 4分节终止序列,在客户机的配合下,成功完成网络连接的关闭,自此通信结束。
对于客户机程序而言,编程步骤如下:
调用 socket,创建一个不与任何网络连接相对应的套接口(socket);调用 connect,将服务器 IP和端口号作为参数输入,系统将选择一个本机尚未使用的大于 1024的端口号作为本地端口号(此时,5元组已经齐备),主动发起连接(这将激发3次握手),当3次握手成功完成后,connect 将返回,此时 socket 已经物理上对应了一个网络连接通道;调用 write 向 socket 写入数据,该数据将达到服务器,被 read所读取,然后调用 read,等待从 socket 获取服务器通过 write 回送的数据;当所有数据都接收完毕,调用 close,激发或者配合完成 4分节终止序列,成功完成网络连接的关闭,自此通信结束。