TCP/IP、UDP设计模式
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。 数组
TCP/IP协议存在于OS中,网络服务经过OS提供,在OS中增长支持TCP/IP的系统调用——Berkeley套接字,如Socket,Connect,Send,Recv等浏览器
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。如图:服务器
TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通讯的中间软件抽象层。网络
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,均可以用“打开open –> 读写write/read –> 关闭close”模式来操做。Socket就是该模式的一个实现, socket便是一种特殊的文件,一些socket函数就是对其进行的操做(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通讯的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来讲,一组简单的接口就是所有,让Socket去组织数据,以符合指定的协议。
数据结构
对于每一个程序系统都有一张单独的表。精确地讲,系统为每一个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在之后操做该文件时使用它。操做系统把该描述符做为索引访问进程描述符表,经过指针找到保存该文件全部的信息的数据结构。socket
SOCKET接口函数ide
工做原理:“open—write/read—close”模式。函数
服务器端先初始化Socket,而后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端链接。在这时若是有个客户端初始化一个Socket,而后链接服务器(connect),若是链接成功,这时客户端与服务器端的链接就创建了。客户端发送数据请求,服务器端接收请求并处理请求,而后把回应数据发送给客户端,客户端读取数据,最后关闭链接,一次交互结束。测试
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。函数的三个参数分别为:
sockfd:即socket描述字,它是经过socket()函数建立了,惟一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址建立socket时的地址协议族的不一样而不一样,如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
addrlen:对应的是地址的长度。
一般服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就能够经过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为何一般服务器端在listen以前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
注意:
网络字节序以大端模式传输。因此:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序
做为一个服务器,在调用socket()、bind()以后就会调用listen()来监听这个socket,若是客户端这时调用connect()发出链接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket能够排队的最大链接个数。socket()函数建立的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的链接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端经过调用connect函数来创建与TCP服务器的链接。
TCP服务器端依次调用socket()、bind()、listen()以后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()以后就向TCP服务器发送了一个链接请求。TCP服务器监听到这个请求以后,就会调用accept()函数取接收请求,这样链接就创建好了。以后就能够开始网络I/O操做了,即类同于普通文件的读写I/O操做。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器链接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。固然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
参数addr
这是一个输出型参数,它用来接受一个返回值,这返回值指定客户端的地址,固然这个地址是经过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。若是对客户的地址不感兴趣,那么能够把这个值设置为NULL。
参数len
如同你们所认为的,它也是输出型参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。一样的,它也能够被设置为NULL。
若是accept成功返回,则服务器与客户已经正确创建链接了,此时服务器经过accept返回的套接字来完成与客户的通讯。
注意:
accept默认会阻塞进程,直到有一个客户链接创建后返回,它返回的是一个新可用的套接字,这个套接字是链接套接字。
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上能够把上面的其它函数都替换成这两个函数。它们的声明以下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
这几个函数比较简单,就不做详细介绍了。
在服务器与客户端创建链接以后,会进行一些读写操做,完成了读写操做就要关闭相应的socket描述字,比如操做完打开的文件要调用fclose关闭打开的文件。
#include <unistd.h>
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,而后当即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再做为read或write的第一个参数。
注意:close操做只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止链接请求。
示例:
TCP通讯
服务器
//sever.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #include <string.h> void* handler_data(void* arg) { int sock = *((int*)arg); printf("connect a new client %d\n", sock); char buf[1024]; memset(buf, '\0', sizeof(buf)); while(1) { ssize_t _s = read(sock, buf, sizeof(buf)-1); if(_s > 0) { buf[_s] = '\0'; printf("client[%d] # %s\n", sock, buf); write(sock, buf, strlen(buf)); } else if(_s == 0) { printf("client[%d] is closed...\n", sock); break; } else { break; } } close(sock); pthread_exit(NULL); } int main() { int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if(listen_sock < 0) { perror("socket"); return 1; } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(8080); local.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(listen_sock, (const struct sockaddr*)&local, sizeof(local)) < 0) { perror("bind"); return 2; } if(listen(listen_sock, 5) < 0) { perror("listen"); return 3; } struct sockaddr_in peer; socklen_t len = sizeof(peer); while(1) { int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len); if(new_fd > 0) { pthread_t id; pthread_create(&id, NULL, handler_data, (void* )&new_fd); pthread_detach(id); } } return 0; }
客户端
//client.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #include <string.h> int main(int argc, char* argv[]) { if(argc != 3) { printf("error argv\n"); return 1; } int conn_sock = socket(AF_INET, SOCK_STREAM, 0); if(conn_sock < 0) { perror("socket"); return 2; } struct sockaddr_in remote; remote.sin_family = AF_INET; remote.sin_port = htons(atoi(argv[2])); remote.sin_addr.s_addr = inet_addr(argv[1]); if(connect(conn_sock, (const struct sockaddr*)&remote, sizeof(remote)) < 0) { perror("connect"); return 3; } char buf[1024]; memset(buf, '\0', sizeof(buf)); while(1) { printf("please enter# "); fflush(stdout); ssize_t _s = read(0, buf, sizeof(buf)-1); if(_s > 0) { buf[_s-1] = '\0'; write(conn_sock, buf, strlen(buf)); read(conn_sock, buf, sizeof(buf)); printf("sever echo# %s\n", buf); } } return 0; }
程序演示:
运行服务器后,服务器等待TCP链接,这里能够用三种方式测试:Telnet、浏览器、客户端。
Telnet测试:
浏览器测试:
客户端测试:
注意:在启动服务器的时候可能会出现以下的状况:
如今用Ctrl-C把client终止掉,等待大约30秒后,服务器又能够启动了。
缘由分析:
虽然server的应用程序终止了,但TCP协议层的链接并无彻底断开,所以不能再次监 听一样的server端口。
client终止时自动关闭socket描述符,server的TCP链接收到client发送的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭链接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,由于咱们先Ctrl-C终止了server,因此server是主动关闭链接的一方,在TIME_WAIT期间仍然不能再次监听一样的server端口。MSL在RFC1122中规定为两分钟,可是各操做系统的实现不一样,在Linux上通常通过半分钟后就能够再次启动server了。
解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示容许建立端口号相同但IP地址不一样的多个socket描述符。在server代码的socket()和bind()调用之间插入以下代码:
setsocketopt这个函数这里不做详细介绍,有兴趣的读者能够自行查询一下。