一、socket编程
socket编程socket这个词可以表示很多概念: 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP地址+端口号”就称为socket。
在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socketpair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连接的一对一关系。
二.端口号分类
(1).熟知端口号:数值在0~1024之间,由IANA把这些端口号分配给TCP/IP中最重要的一些应用程序,让所有的用户都能知道。(一般在服务器上绑定。)
(2).登记端口号:数值在1024~49151之间,这类端口号是为没有熟知端口号的应用程序使用的。使用这类端口号必须在IANA按照规定的手续登记,以防止重复。49151是65535的四分之三。(一般在服务器上使用)
(3).短暂端口号:数值在49152~65535之间,这类端口号是在客户程序运行时才动态选择,当通信结束时,这个刚才使用的端口号会被系统回收,以供其它客户进程使用。(在客户端使用)
三、网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式, 地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8, 也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的, 接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转 换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
四、socket地址的数据类型及相关函数
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而各种网络协议的地址格式并不相同,如下图所示
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各 种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现 都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和UNIXDomain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的 内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址。但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间转换。
TCP协议通讯流程在我的上一篇博客中详细的介绍了,本文不再赘述。
五、最简单的TCP网络应用程序
server.c 的作用是接受client的请求,并与client进行简单的数据通信,整体为一个阻塞式的网络聊天工具。
1、socket函数
对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表是面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协 议。protocol参数的介绍从略,指定为0即可。
2、bind函数
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。struct sockaddr *是一个通过指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,它们的长度各不相同,所以需要第三个参 数addrlen指定结构体的长度。
设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址,这样设置可以在所有的IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底是哪个IP地址,端口号我们定义为8080。
3、listen函数
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog(宏)个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
4、accept函数
三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。成功返回一个新创建的scoket的描述符,不再需要监听该scoket,继续去接受scoket。
所以server的创建流程为:
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void * thread_run(void * arg)
{
char buf[1024];
int fd = (int)arg;
printf("create a new thread...\n");
while(1)
{
ssize_t _s = read(fd,buf,sizeof(buf));
if(_s > 0)
{
buf[_s-1] ='\0';
}
else if(_s == 0)
{
printf("client close...\n");
//close(fd);
break;
}
else
{
break;
}
printf("client :%s\n",buf);
write(fd,buf,strlen(buf));
}
close(fd);
pthread_exit(0);
}
int main()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("scok");
return -1;
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = inet_addr("192.168.234.129");
//local.sin_addr.s_addr = htonl(INADDR_ANY);
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//bind
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
close(sock);
return 2;
}
//listen
if(listen(sock,5)< 0)
{
perror("listen");
close(sock);
return 3;
}
printf("bind and listen succes,wait accept..\n");
struct sockaddr_in client_sock;
socklen_t len = sizeof(client_sock);
while(1)
{
int new_fd = accept(sock,(struct sockaddr*)&client_sock,&len);
if(new_fd < 0)
{
perror("accept");
close(sock);
return 3;
}
printf("get connection ,ip is :%s port is :%d\n",inet_ntoa(client_sock.sin_addr.s_addr),client_sock.sin_port);
//create pthread
pthread_t id;
if(pthread_create(&id,NULL,thread_run,(void*)new_fd) < 0)
{
perror("phthread_create");
close(sock);
return 4;
}
//线程分离
pthread_detach(id);
}
close(sock);
return 0;
}
connect函数
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对放的地址。connect()成功返回0,出错返回-1。
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char* argv[])
{
if(argc !=3)
{
printf("Usage : server [IP] [PORT] \n");
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("scok");
return -1;
}
struct sockaddr_in server_sock;
server_sock.sin_family = AF_INET;
server_sock.sin_port = htons(atoi(argv[2]));
server_sock.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sock,(struct sockaddr*)&server_sock,sizeof(server_sock));
if(ret < 0)
{
perror("connect");
close(sock);
return 2;
}
printf("connect success...\n");
char buf[1024];
while(1)
{
printf("please enter :");
fflush(stdout);
ssize_t _s = read(0,buf,sizeof(buf));
if(_s > 0)
{
buf[_s] ='\0';
}
else if(_s == 0)
{
close(sock);
break;
}
else
{
break;
}
write(sock,buf,strlen(buf));
printf("please wait...\n");
read(sock,buf,sizeof(buf));
printf("server # :%s",buf);
}
return 0;
}
首先运行服务器查看监听状态
运行client端,给server端发送消息
server端显示效果
这次实现的简单的TCP服务器使用的是多线程,
多线程的优点:
无需跨进程边界;
程序逻辑和控制方式简单;
所有线程可以直接共享内存和变量等;
线程方式消耗的总资源比进程方式好;
多线程缺点:
每个线程与主程序共用地址空间,受限于2GB地址空间;
线程之间的同步和加锁控制比较麻烦;
一个线程的崩溃可能影响到整个程序的稳定性;
到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
多进程优点:
每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
通过增加CPU,就可以容易扩充性能;
可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多进程缺点:
逻辑控制复杂,需要和主程序交互;
需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
多进程调度开销比较大;
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。