TCP/IP协议集成到操作系统的内核中,TCP/IP协议中引入了一种称之为"Socket(套接字)"应用程序接口"。端口在计算机编程上也就是"Socket接口"。计算机可以通过应用层程序(调用Socket接口)的方式进行通信。
协议由三要素组成:
[1] 语法,即数据与控制信息的结构或格式;
[2] 语义,即需要发出何种控制信息,完成何种动作以及做出何种响应;
[3] 时序(同步),即事件实现顺序的详细说明。
协议总是指某一层的协议。准确地说,它是在同等层之间的实体通信时,有关通信规则和约定的集合就是该层协议。
2. practice
(1) 基于TCP协议的客户端/服务器程序流程
基于TCP协议程序意思:Call Socket API to write an application of TCP protocol. Why it will use ip address(socket地址)?Because在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。
三方握手
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
ifconfig
The "inet addr" of lo is used to client's connect function which used to describe server's ip address. From the picture, we can use 127.x.x.x because mask(255.0.0.0).
server.c
/*Filename: server.c
*Brife: Be server response client's request based on tcp protocol
*Author: One fish
*Date: 2014.9.8 Mon
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
#define END_S "QUIT\n"
#define END_B "quit\n"
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE] = "hello";
char str[INET_ADDRSTRLEN];
int i, n;
//Use IPv4 format to be server's address(AF_INET),
//use tcp protocol to transmit data between server and client(SOCK_STREAM)
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//Set server's ip address and port values
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//Bind server's network port(socket) with its port and address
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (-1 == r_flag) {
fputs("Bind server ip address and port to socket port failed\n", stderr);
exit(1);
}
//Set the numbers of client connect to server
listen(listenfd, 20);
if (-1 == r_flag) {
fputs("Set listen number failed\n", stderr);
exit(1);
}
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
//Return after three times hand-shaking
connfd = accept(listenfd, (struct sockaddr *)&cliaddr,
&cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str,
sizeof(str)),
ntohs(cliaddr.sin_port));
//Response to client
while ( strcmp(buf,END_S) && strcmp(buf, END_B) ) {
memset(buf, '\0', sizeof(buf) );
n = read(connfd, buf, MAXLINE);
do{
printf("From client's infor: %sYour reply(Can't be NULL):", buf);
}while( NULL == fgets(rbuf, MAXLINE - 2, stdin) );
write(connfd,rbuf, strlen(rbuf));
}
printf("Close socket\n");
close(connfd);
}
}
|
| int socket(int family, int type, int protocol); |
| int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); |
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器(socket)需要调用bind绑定一个固定的网络地址和端口号。bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
| bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); |
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为8000。网络数据流同样有大端小端之分,网 络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。如果发送主机是小端字节序的,发 送主机把1000填到发送缓冲区之前需要做字节序的转换为大端。可以调用以下库函数做网络字节序和主机字节序的转换。
| #include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); |
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
| int listen(int sockfd, int backlog); |
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
| int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); |
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。
client.c
|
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
| int connect(int sockfd, const struct sockaddr *servaddr,socklen_t addrlen); |
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
先编译运行服务器:
| lly@debian:~/mydir/lly_books/linux_c_programming_osl/socket$gcc server.c -o server lly@debian:~/mydir/lly_books/linux_c_programming_osl/socket$./server Accepting connections ... |
编译运行客户端:
| root@debian:/home/lly/mydir/lly_books/linux_c_programming_osl/socket#gcc client.c -o client root@debian:/home/lly/mydir/lly_books/linux_c_programming_osl/socket#./client |
The result:
server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。
在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:8000)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:8000),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()调用之间插入如下代码:
| int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
服务器通常是要同时服务多个客户端,网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。
while (1) {
cliaddr_len = sizeof(cliaddr);
//Return after three times hand-shaking
connfd = accept(listenfd, (struct sockaddr *)&cliaddr,
&cliaddr_len);
//Use one process to manage one client
r_flag = fork();
if (-1 == r_flag) {
perror("fork");
exit(1);
} else if (0 == r_flag) { //In child process
close(listenfd); //Save socket only in panrent process
pid = getpid();
//Print client's information
printf("\nServer-%d received from %s at PORT %d\n",
pid,
inet_ntop(AF_INET, &cliaddr.sin_addr, str,
sizeof(str)),
ntohs(cliaddr.sin_port));
//Response to client
while ( strcmp(buf,END_S) && strcmp(buf, END_B) ) {
memset(buf, '\0', sizeof(buf) );
n = read(connfd, buf, MAXLINE);
do{
printf("From client's infor: %s\nServer-%d reply(Can't be NULL):", buf, pid);
}while( NULL == fgets(rbuf, MAXLINE - 2, stdin) );
write(connfd,rbuf, strlen(rbuf));
}
printf("Close socket, Server-%d exit to be zombies\n", pid);
close(connfd);
//Child process exit to be zombies
exit(1);
} else { //In prant process
close(connfd);
}
}
|
So the code can not be used code.
[2014.9.8-16:44]
LCNote Over.