所有知名服务使用的端口号都定义在/etc/services文件中。

由socket定义的API提供两点功能:一是将应用程序数据从用户缓冲区中复制到TCP/IP内核发送缓冲区,以交付内核来发送数据,或者从内核TCP/UDP接收缓冲区中复制数据到用户缓冲区,以读取数据;二是应用程序可以通过它们来修改内核中各层协议的某些头部信息或其他数据结构。

int socket(int domain, int type, int protocol);

当调用socket创建一个socket时,如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址。(套接字数据结构都存放在操作系统的内核缓冲里)

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind函数把一个地址族中的特定地址赋给socket。通常服务器在启动的时候都会绑定一个众所周知的地址(如ipdiz+端口号),而客户端就不用指定,系统自动分配一个端口号和自身的IP地址组合。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

socket()函数创建的socket默认是一个主动类型的,listen()函数将socket变为被动类型的,等待客户连接。客户端通过调用connect()函数来建立与TCP服务器的连接。

#include <unistd.h>
int close(int fd);

close一个TCP socket的默认行为,会把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由进程使用,也就是不能再作为read或write的第一个参数。close操作只是使相应socket描述符的引用计数-1,只有当引用计数为0时,才会触发TCP客户端向服务器发送终止连接请求。

Linux编程入门三网络编程一_#include

调用accept函数从accept队列中取出一个节点,并分配一个fd。

每个TCP套接字都有一个发送缓冲区和一个接收缓冲区。从写一个TCP套接字的write调用成功后返回,仅仅表示可以重新使用原来的应用进程缓冲区了,并不代表对端TCP或应用进程已接收到数据。对端TCP必须确认收到的数据,伴随来自对端的ACK不断到达,本端TCP至此才能从套接字发送缓冲区中丢弃已确认的数据。TCP必须为已发送的数据保留一个副本,直到它被对端确认为止。

每个UDP套接字有一个接收缓冲区。UDP不保存应用进程数据的副本,因此无需一个真正的发送缓冲区。write调用成功返回表示所写的数据报或其所有分片已被加入数据链路层的输出队列。

对于read调用(套接字标志为阻塞),如果接收缓冲区中有20 byte空间,请求读100 byte的数据,就会返回20。对于write调用(套接字标志为阻塞),如果请求写100 byte的数据,而发送缓冲区中只有20 byte的空闲位置,那么write会阻塞,直到把100 byte的数据全部交给发送缓冲区才返回,如果write中得到套接字标志为非阻塞,则直接返回20。

封包和解包
TCP是流协议,所谓流,就是没有界限的一串数据。TCP由于流的特性以及网络状况,在进行数据传输时假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况)

  • 先接收到data1,然后接收到data2
  • 先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部。
  • 先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据。
  • 一次性接收到了data1和data2的全部数据
    上面三个情况就是常说的粘包,就需要把接收的数据进行拆包,拆成一个个独立的数据包。

同步IO指的是,必须等待IO操作完成后,控制权才返回给用户进程。异步IO指的是,无须等待IO操作完成,就将控制权返回给用户进程。
4种IO模型:1. 阻塞IO模型 2. 非阻塞IO模型 3. 多路IO复用模型 4. 异步IO模型

阻塞IO模型

在Linux中,默认情况下所有的socket都是阻塞的。几乎所有IO接口都是阻塞型的。阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,不需要等到IO操作彻底完成。

Linux编程入门三网络编程一_网络编程_02


执行完bind()和listen()后,操作系统已经开始在指定的端口处监听所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用accept()接口从socket fd的请求队列抽取第一个连接信息,创建一个与fd同类的新的socket返回句柄,这个socket句柄是后续read()和recv()的输入参数。如果请求队列当前没有请求,则accept()将进入阻塞状态直到有请求进入队列。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的第一个参数为服务器的socket描述字。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。一个服务器通常仅创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
server.cpp代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#define MAXLINE 4096
int main(int argc, char** argv){
int listenfd, acceptfd;
struct sockaddr_in servaddr;
if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
return -1;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
return -1;
}
if( listen(listenfd, 10) == -1){
printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
return -1;
}
printf("======waiting for client's request======\n");
if( (acceptfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
}else{
printf("accept succ\n");
int rcvbuf_len;
socklen_t len = sizeof(rcvbuf_len);
if( getsockopt( acceptfd, SOL_SOCKET, SO_RCVBUF, (void *)&rcvbuf_len, &len ) < 0 ){
perror("getsockopt: " );
}
printf("the recv buf len: %d\n" , rcvbuf_len );
}
char recvMsg[246988]={0};
ssize_t totalLen = 0;
while (1) {
sleep(1);
ssize_t readLen = read(acceptfd, recvMsg, sizeof(recvMsg));
printf("readLen:% ld\n",readLen);
if (readLen < 0) {
perror("read: ");
return -1;
}else if (readLen == 0) {
printf("read finish: len = % ld\n",totalLen);
close(acceptfd);
return 0;
}else{
totalLen += readLen;

}
}
close(acceptfd);
return 0;
}

client.cpp

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#define MAXLINE 4096
int main(int argc, char** argv){
int connfd, n;
char recvline[4096], sendline[4096];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
return 0;
}
if( (connfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
return 0;
}
if(connect(connfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
ssize_t writeLen;
char sendMsg[246988] = {0};
int count = 0;
while (1){
count++;
if (count == 5) {
return 0;
}
writeLen = write(connfd, sendMsg, sizeof(sendMsg));
if (writeLen < 0) {
printf("write failed\n");
close(connfd);
return 0;
}
else{
printf("write sucess, writelen:%d\n",writeLen);
}
}
close(connfd);
return 0;
}

非阻塞IO模型

在Linux下,可以通过设置socket使IO变为非阻塞状态。使用如下函数可以将某句柄fd设为非阻塞状态。

fcntl(fd,F_SETFL,O_NONBLOCK);

Linux编程入门三网络编程一_套接字_03

多路IO复用模型

I/O复用是最常用的I/O通知机制,应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。

多路IO复用,有时也称为事件驱动IO。它的基本原理就是有个函数(如select)会不断地轮询所负责的所有socket,当某个socket有数据到达,就通知用户进程。

Linux编程入门三网络编程一_#include_04


当用户进程调用select,那么整个进程会被阻塞,而同时,内核会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。

在多路复用IO模型中,对于每个socket,一般都设置为非阻塞,整个用户的进程其实是一直被阻塞的。只不过进程是被select函数阻塞,而不是被socket IO阻塞。

FD_ZERO(fd_set* fds); //将set清零
FD_SET(int fd, fd_set* fds); //将fd加入fds
FD_ISSET(int fd, fd_set* fds); //如果fd在fds中则为真
FD_CLR(int fd, fd_set* fds); //将fd从fds中清除
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

这里fd_set类型可以理解为按bit位标记句柄的队列。在select函数中,readfds、writefds、exceptfds同时作为输入参数和输出参数。如果输入的readfds标记了16号句柄,则select将检测16号句柄是否可读。在select返回后,可以通过检查readfds是否标记16号句柄,来判断该可读事件是否发生。客户端一个connect操作,将在服务器端激发一个可读事件,所以select也能检测来自客户端的connect行为。

Linux编程入门三网络编程一_网络编程_05


这种模型的特征在于每个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体过于庞大,则对整个模型是灾难性的。

int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);

这里用到了两个结构体:fd_set和timeval。结构体fd_set可以理解为一个集合,这个集合中存放的是文件描述符,即文件句柄。结构体timeval是一个常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。

  • maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
  • readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符。因为要监视文件描述符的读变化,即关心是否可以从这些文件中读取数据,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读。如果没有可读的文件,则根据timeout参数再判断是否超时:若超出timeout的时间,select返回0;若发生错误返回负值;也可传入null值,表示不关心任何文件的读变化。
  • writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符。因为要监视文件描述符的写变化,即关心是否可以从这些文件中写入数据,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写。如果没有可写的文件,则根据timeout参数再判断是否超时:若超出timeout的时间,select返回0;若发生错误返回负值;也可传入null值,表示不关心任何文件的写变化。
  • errorfds同上面两个参数的意图,用来监视文件错误异常
  • timeout是select的超时时间,这个参数至关重要,它可以使select处于3种状态:1.若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止 2.若将时间值设置为0,就变成一个纯粹的非阻塞函数,不管文件描述符是否变化,都立即返回继续执行,文件无变化返回0,有变化返回一个正值 3.timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回,否则在超时后不管怎样一定返回,返回值同上。
  • 返回值:准备就绪的描述符数,若超时则返回0,若出错则返回-1

使用select函数循环读取键盘输入,/dev/tty代表当前终端,如果超过5s不输入字符,程序自动打印time out。

  1#include <sys/time.h>
2 #include <stdio.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <assert.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <unistd.h>
10 #include <sys/socket.h>
11 #include <netinet/in.h>
12 #include <arpa/inet.h>
13 #include <errno.h>
14 #include <sys/select.h>
15 int main()
16 {
17 int keyboard;
18 int ret,i;
19 char c;
20 fd_set readfd;
21 struct timeval timeout;
22 keyboard = open("/dev/tty", O_RDONLY|O_NONBLOCK);
23 assert(keyboard>0);
24 while(1)
25 {
26 timeout.tv_sec = 5;
27 timeout.tv_usec = 0;
28 FD_ZERO(&readfd);
29 FD_SET(keyboard,&readfd);
30 ret = select(keyboard+1,&readfd,NULL,NULL,&timeout);
31 if(ret == -1)
32 {
33 perror("select error");
34 }else if(ret == 0){
35 printf("time out\n");
36 }else{
37 if(FD_ISSET(keyboard,&readfd))
38 {
39 i = read(keyboard,&c,1);
40 if('\n'==c)
41 continue;
42 printf("the input is %c\n",c);
43 if('q'==c)
44 break;
45 }
46 }
47 }
48 }

使用select函数实现服务器
server.cpp

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#define DEFAULT_PORT 6666
int main( int argc, char ** argv){
int serverfd,acceptfd; /* 监听socket: serverfd,数据传输socket: acceptfd */
struct sockaddr_in my_addr; /* 本机地址信息 */
struct sockaddr_in their_addr; /* 客户地址信息 */
unsigned int sin_size, myport=6666, lisnum=10;
if ((serverfd = socket(AF_INET , SOCK_STREAM, 0)) == -1) {
perror("socket" );
return -1;
}
printf("socket ok \n");
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(DEFAULT_PORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero), 0);
if (bind(serverfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr )) == -1) {
perror("bind" );
return -2;
}
printf("bind ok \n");
if (listen(serverfd, lisnum) == -1) {
perror("listen" );
return -3;
}
printf("listen ok \n");
fd_set client_fdset; /*监控文件描述符集合*/
int maxsock; /*监控文件描述符中最大的文件号*/
struct timeval tv; /*超时返回时间*/
int client_sockfd[5]; /*存放活动的sockfd*/
bzero((void*)client_sockfd,sizeof(client_sockfd));
int conn_amount = 0; /*用来记录描述符数量*/
maxsock = serverfd;
char buffer[1024];
int ret=0;
while(1){
/*初始化文件描述符号到集合*/
FD_ZERO(&client_fdset);
/*加入服务器描述符*/
FD_SET(serverfd,&client_fdset);
/*设置超时时间*/
tv.tv_sec = 30; /*30秒*/
tv.tv_usec = 0;
/*把活动的句柄加入到文件描述符中*/
for(int i = 0; i < 5; ++i){
/*程序中Listen中参数设为5,故i必须小于5*/
if(client_sockfd[i] != 0){
FD_SET(client_sockfd[i], &client_fdset);
}
}
/*printf("put sockfd in fdset!\n");*/
/*select函数*/
ret = select(maxsock+1, &client_fdset, NULL, NULL, &tv);
if(ret < 0){
perror("select error!\n");
break;
}
else if(ret == 0){
printf("timeout!\n");
continue;
}
/*轮询各个文件描述符*/
for(int i = 0; i < conn_amount; ++i){
/*FD_ISSET检查client_sockfd是否可读写,>0可读写*/
if(FD_ISSET(client_sockfd[i], &client_fdset)){
printf("start recv from client[%d]:\n",i);
ret = recv(client_sockfd[i], buffer, 1024, 0);
if(ret <= 0){
printf("client[%d] close\n", i);
close(client_sockfd[i]);
FD_CLR(client_sockfd[i], &client_fdset);
client_sockfd[i] = 0;
}
else{
printf("recv from client[%d] :%s\n", i, buffer);
}
}
}
/*检查是否有新的连接,如果收,接收连接,加入到client_sockfd中*/
if(FD_ISSET(serverfd, &client_fdset)){
/*接受连接*/
struct sockaddr_in client_addr;
size_t size = sizeof(struct sockaddr_in);
int sock_client = accept(serverfd, (struct sockaddr*)(&client_addr), (unsigned int*)(&size));
if(sock_client < 0){
perror("accept error!\n");
continue;
}
/*把连接加入到文件描述符集合中*/
if(conn_amount < 5){
client_sockfd[conn_amount++] = sock_client;
bzero(buffer,1024);
strcpy(buffer, "this is server! welcome!\n");
send(sock_client, buffer, 1024, 0);
printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
bzero(buffer,sizeof(buffer));
ret = recv(sock_client, buffer, 1024, 0);
if(ret < 0){
perror("recv error!\n");
close(serverfd);
return -1;
}
printf("recv : %s\n",buffer);
if(sock_client > maxsock){
maxsock = sock_client;
}
else{
printf("max connections!!!quit!!\n");
break;
}
}
}
}
for(int i = 0; i < 5; ++i){
if(client_sockfd[i] != 0){
close(client_sockfd[i]);
}
}
close(serverfd);
return 0;
}

client.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define DEFAULT_PORT 6666
int main( int argc, char * argv[]){
int connfd = 0;
int cLen = 0;
struct sockaddr_in client;
if(argc < 2){
printf(" Uasge: clientent [server IP address]\n");
return -1;
}
client.sin_family = AF_INET;
client.sin_port = htons(DEFAULT_PORT);
client.sin_addr.s_addr = inet_addr(argv[1]);
connfd = socket(AF_INET, SOCK_STREAM, 0);
if(connfd < 0){
perror("socket" );
return -1;
}
if(connect(connfd, (struct sockaddr*)&client, sizeof(client)) < 0){
perror("connect" );
return -1;
}
char buffer[1024];
bzero(buffer,sizeof(buffer));
recv(connfd, buffer, 1024, 0);
printf("recv : %s\n", buffer);
bzero(buffer,sizeof(buffer));
strcpy(buffer,"this is client!\n");
send(connfd, buffer, 1024, 0);
while(1){
bzero(buffer,sizeof(buffer));
scanf("%s",buffer);
int p = strlen(buffer);
buffer[p] = '\0';
send(connfd, buffer, 1024, 0);
printf("i have send buffer\n");
}
close(connfd);
return 0;
}

异步IO模型

Linux编程入门三网络编程一_#include_06


当进程发起IO操作之后,就直接返回,直到内核发送一个信号,告诉进程IO已完成,则在这个过程中,进程完全没有被阻塞。

参考:
后台开发核心技术与应用实践 徐晓鑫