Linux网络编程:客户端/服务器的简单实现
一、 Socket的基本知识
1. socket功能
Socket层次
Socket实质上提供了进程通信的端点,进程通信之前,双方必须首先各自创建一个端点,否则是没有办法建立联系并相互通信的。
每一个Socket都一个半相关描述:
{协议, 本地地址, 本地端口}
完整的Socket的描述:
{协议, 本地地址, 本地端口, 远程地址, 远程端口}
2. Socket工作流程
面向连接(TCP)的Socket工作流程
UDP的socket工作流程
l 服务器端
首先,服务器应用程序用系统调用socket()来创建一个socket,它是系统分配给该服务器进程的类似文件描述符的资源,不能与其他进程共享。
接下来,需要给socket绑定,本地socket绑定的是Linux文件系统中的文件名,一般放在/tmp或者/usr/tmp目录中。对于网络socket,要和客户连接的特定网络相关的服务标示符(端口号或者访问点)。可以使用系统调用bind()来绑定socket,然后服务器进程就用listen()创建一个队列将客户的连接存入队列,再使用accept()接收客户的连接。
服务器调用accept()时会创建一个和原有的socket不同的新socket。这个新socket只用于与这个特定的客户进行通信,而原socket保留下来继续处理来自其他客户的连接。
l 客户端
客户端是首先调用socket()创建一个未绑定的socket,然后将服务器的socket作为一个地址调用connect()与服务器建立连接。
3. 套接字属性
l 套接字的域(domain)
l 套接字的类型(type)
套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字。
流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送顺序的数据:“1”、“2”,那么数据到达的顺序也是“1”、“2”。流式套接字在AF_INET域中使用TCP协议来保证数据传输的正确性及顺序性。TCP是TCP/IP协议的前半部分,IP只处理网络路由。
数据报套接字(SOCK_DGRAM)
数据报协议定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。它使用UDP/IP协议。UDP将数据打包,贴上IP地址,然后发送。这个过程不需要建立连接。
原始套接字
原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有流式套接字和数据报套接字使用方便,一般的程序也不涉及到原始套接字。
4. 套接字地址
AF_INET与AF_UNIX域的套接字地址结构不相同,分别为struct sockaddr_in, struct sockaddr_un。
l AF_INET地址结构
#include <netinet/in.h> struct sockaddr_in { short int sin_family; /* AF_INET */ unsigned short int sin_port; /* Port Numbers*/ struct in_addr sin_addr; /* Internet Address */ }
IP地址结构in_add定义为:
struct in_addr { unsigned long int s_addr; /* IP地址是四个字节的一个32位值 */ }
l AF_UNIX地址结构
#include <sys/un.h> struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[]; /* pathname */ }
在当前的Linux系统中,由X/Open规范定义的类型sa_family_t在头文件sys/un.h中声明,它是短整数类型。另外sun_path指定的路径名长度也是有限制的(Linux规定的是108个字符)。
二、 转换函数
1. 主机字节序和网络字节序
因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,低位在后,有的系统是的低位在前,高位在后),而网络传输的字节序需要统一。所以,对于主机字节序和网络字节序不同的机器,就一定要对数据进行转换(例如IP地址的表示和端口号的表示)。如果主机字节序和网络字节序相同,也要调用转换函数,真正转换 还是不转换由系统函数自己决定。
转换函数:
#include <netinet/in.h> unsigned long int htonl(unsigned long int hostlong); /* host to network long */ unsigned short int htons(unsinged short int hostshort); /* host to network short */ unsigned long int ntohl(unsigned long int netlong); /* network to host long */ unsigned short int ntohs(unsigned short int netshort); /* network to host short */
这些函数将16位和32位整数在主机字节序和标准的网络字节序之间进行转换。“h”代表主机“host”,“n”代表网络“network”,“l”代表“long”,“s”代表“short”。
三、 socket系统调用
1. 创建套接字socket()
socket()系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
创建的套接字是一条通信线路的一个端点,domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议。
最常用的套接字域是AF_UNIX和AF_INET,前者用于通过UNIX和Linux文件系统实现的本地套接字,后者用于UNIX网络套接字。AF_INET套接字可以用于通过包括互联网在内的TCP/IP网络进行通信的程序。
参数type指定这个socket的通信类型,protocol参数指定使用的协议。通信所需的协议一般是由socket类型来决定,通常不需要进行选择。只有当需要选择的时候,才会用到protocol参数。将protocol参数设置为0表示使用默认协议。
socket返回一个描述符,类似于文件描述符。这个描述符可以用于read(),write()等系统调用来连接另一个socket。
实例:创建socket,AF_INET,SOCK_STREAM。
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
2. 绑定socket
在调用socket()获得描述符之后,需要对该套接字进行绑定。AF_UNIX套接字会关联到一个文件系统的路径名,而AF_INET套接字会关联到一个IP端口号。
#include <sys/socket.h> int bind(int socket, const struct sockaddr *address, size_t address_len);
bind将参数address中的地址分配给与文件描述符socket关联的未命名套接字。address_len传递地址结构体的长度。 地址的长度取决于地址的类型。bind系统调用需要将struct sockaddr_in或struct sockaddr_un指针转换成struct sockaddr *类型。
bind在调用成功时返回0, 失败是返回-1并设置errno。
EBADF |
文件描述符无效 |
ENOTSOCK |
文件描述符对应的不是一个socket |
EINVAL |
文件描述符对应的是一个已经绑定的socket |
EADDRNOTAVAIL |
地址不可用 |
EADDRINUSE |
地址已经绑定了一个socket |
表2 errno值
AF_UNIX还有一些错误代码
EACCESS |
权限不足,不能创建文件系统中的路径名 |
ENOTDIR, ENAMETOOLONG |
文件名不符合要求 |
表3 AF_UNIX部分errno值
实例:
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
3. 创建套接字队列
为了能够在套接字上接受进入的链接,服务器要建立一个队列来保存未处理的请求。
#include <sys/socket.h> int listen(int socket, int backlog);
参数backlog设置队列中可以容纳的未处理连接的最大个数。超过这个数字后,剩下的连接会被拒绝。backlog常用值为5……
listen函数会在成功时返回0,失败时返回-1,错误代码包括EBADF,EINVAL和ENOTSOCK。
实例:
listen(server_sockfd, 5);
4. 接受连接
一旦服务器程序创建并绑定了socket之后,他就可以通过用accept()来等待客户建立对该socket的连接。
#inculde <sys/socket.h> int accept(int socket, struct sockaddr *address, size_t *address_len);
accept只有当有客户程序尝试连接到由socket参数指定的socket上时才返回。accept将创建一个新socket来与该客户进行通信,将该socket描述符作为返回值。之后的读写动作都关联到该socket描述符上。
参数socket所关联的套接字必须首先已经被bind绑定,而且有listen为其分配连接队列。参数address表示客户的地址,如果不关心客户的地址值可设为空指针。
如果socket没有未处理的连接accept将阻塞直到队列中有未处理的连接。可以通过设置O_NONBLOCK来改变。实例:
int flags = fcntl(socket, F_GETFL, 0); fcntl(socket, F_SETFL, O_NONBLOCK | flags);
发生错误时,accept会返回-1。
5. 请求连接
客户程序通过与服务器监听套接字之间绑定的方法连接到服务器。
#include <sys/socket.h> int connect(int socket, const struct sockaddr *address, size_t address_len);
参数socket指定的套接字将连接到参数address指定的服务器的socket上。
成功时,connect返回0,失败返回-1。
如果连接不能立刻建立,connect将阻塞到超时时间,超过超时时间连接将被放弃,连接失败。
6. 关闭socket
可以通过close()来终止服务器与客户端的socket连接。
#include <unistd.h> int close(int socket);
7. 发送数据send()
send()同样可以发送数据,与write()不同的是,send()只能用于socket数据的发送。
#include <sys/socket.h> int send(int socket, const void *buff, int len, int flags)
参数中,buff指向要发送的数据,len为要发送数据的长度, flags一般为0。
成功时send返回发送的字节数,失败返回-1。
8. 接收数据recv()
与send()相同,recv()也只能用于socket的数据发送。
#include <sys/socket.h> int recv(int socket, void *buf, int len, unsigned int flags)
buf指向存放接收数据的缓冲区,len为数据长度,flags一般为0。
成功时recv()返回接收的字节数,失败时返回-1。
发送数据sendto()
sendto需要带上发送目的地的地址信息,可以用于UDP通讯的实现,TCP中也可以使用sendto()。
#include <sys/socket.h> int sendto(int socket, const void *buff, int len, unsigned int flags, const struct sockaddr *addr_to, int addr_len)
buff指向要发送的数据,len为要发送的数据的长度,flags一般为0,addr_to携带发送目的IP的信息,addr_len是地址信息的长度。
成功时,sendto返回发送的字节数,失败返回-1。
接收数据recvfrom()
recvfrom()与sendto配套使用,实现数据的收发。
#include <sys/socket.h> int recvfrom(int socket, const void *buff, int len, unsigned int flags, const struct sockaddr *addr_from, int addr_len)
buff指向接收数据的缓冲区,len为数据长度,flags一般为0, addr_from存放数据来源的IP地址,addr_len为地址信息的长度。
recvfrom成功时返回接收的字节数,失败返回-1。
四、 阻塞
connect(),recv()都是阻塞性函数,当需求的资源没有准备好的时候,调用函数的进程将进入休眠状态,这样就无法处理I/O多路复用的情况了。
解决这个问题的方法与普通的文件操作相同:使用fcntl()或者select()函数。相比较fcntl(),select()函数还可以设置等待时间,功能更为强大。
----<end>----
/*
TCP通信的基本步骤如下:
服务端:socket---bind---listen---while(1){---accept---recv---send---close---}---close
客户端:socket----------------------------------connect---send---recv-----------------close
头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
socket函数:生成一个套接口描述符
原型:int socket(int domain,int type,int protocol);
参数:domain{ AF_INET:Ipv4网络协议 AF_INET6:IPv6网络协议}
type{tcp:SOCK_STREAM udp:SOCK_DGRAM}
protocol指定socket所使用的传输协议编号。通常为0.
返回值:成功则返回套接口描述符,失败返回-1。
常用实例:
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
bind函数:用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。
原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
参数:sockfd为前面socket的返回值。
my_addr为结构体指针变量
对于不同的socket domain定义了一个通用的数据结构
struct sockaddr //此结构体不常用
{
unsigned short int sa_family; //调用socket()时的domain参数,即AF_INET值。
char sa_data[14]; //最多使用14个字符长度
};
此sockaddr结构会因使用不同的socket domain而有不同结构定义,
例如使用AF_INET domain,其socketaddr结构定义便为
struct sockaddr_in //常用的结构体
{
unsigned short int sin_family; //即为sa_family AF_INET
uint16_t sin_port; //为使用的port编号
struct in_addr sin_addr; //为IP 地址
unsigned char sin_zero[8]; //未使用
};
struct in_addr
{
uint32_t s_addr;
};
addrlensockaddr的结构体长度。通常是计算sizeof(struct sockaddr);
返回值:成功则返回0,失败返回-1
常用实例:
struct sockaddr_in my_addr; //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
//或bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用Ipv4网络协议
my_addr.sin_port = htons(8888); //表示端口号为8888,通常是大于1024的一个值。
//htons()用来将参数指定的16位hostshort转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101");
// inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为INADDR_ANY,这表示服务器自动填充本机IP地址。
if(bind(sfd, (struct sockaddr*)&my_addr, sizeof(struct socketaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}
(注:通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。
同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。)
listen函数:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
原型:int listen(int sockfd, int backlog);
参数:sockfd为前面socket的返回值.即sfd
backlog指定同时能处理的最大连接要求,通常为10或者5。 最大值可设至128
返回值:成功则返回0,失败返回-1
常用实例:
if(listen(sfd, 10) == -1)
{
perror("listen");
close(sfd);
exit(-1);
}
accept函数:接受远程计算机的连接请求,建立起与客户机之间的通信连接。
服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,
而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,
会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,
继续监听其它客户机的连接请求。
(类似于移动营业厅,如果有客户打电话给10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,
也就是说,后面的所有操作,此时已经于服务器没有关系,而是话务员跟客户的交流。对应过来,客户请求连接我们的服务器,
我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用accept函数产生一个新的套接字,
然后用这个新的套接字跟我们的客户进行收发数据。也就是说,服务器跟一个客户端连接成功,会有两个套接字。)
原型:int accept(int s,struct sockaddr * addr,int * addrlen);
参数:
s为前面socket的返回值.即sfd
addr为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
addrlen表示结构体的长度,为整型指针
返回值:成功则返回新的socket处理代码new_fd,失败返回-1
常用实例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
recv函数:用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
参数:sockfd为前面accept的返回值.即new_fd,也就是新的套接字。
buf表示缓冲区
len表示缓冲区的长度
flags通常为0
返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1
常用实例:
char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{
perror("recv");
close(new_fd);
close(sfd);
exit(-1);
}
puts(buf);
send函数:用新的套接字发送数据给指定的远端主机
原型:int send(int s,const void * msg,int len,unsigned int flags);
参数:s为前面accept的返回值.即new_fd
msg一般为常量字符串
len表示长度
flags通常为0
返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1
常用实例:
if(send(new_fd, "hello", 6, 0) == -1)
{
perror("send");
close(new_fd);
close(sfd);
exit(-1);
}
close函数:当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源
原型:int close(int fd);
参数:fd为前面的sfd,new_fd
返回值:若文件顺利关闭则返回0,发生错误时返回-1
常用实例:
close(new_fd);
close(sfd);
connect函数:用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去。
原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
参数:
sockfd为前面socket的返回值,即sfd
serv_addr为结构体指针变量,存储着远程服务器的IP与端口号信息。
addrlen表示结构体变量的长度
返回值:成功则返回0,失败返回-1
常用实例:
struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服务器的ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{
perror("connect");
close(sfd);
exit(-1);
}
Linux网络编程:客户端/服务器的简单实现的更多相关文章
- Linux网络编程客户\服务器设计范式
1.前言 网络编程分为客户端和服务端,服务器通常分为迭代服务器和并发服务器.并发服务器可以根据多进程或多线程进行细分,给每个连接创建一个独立的进程或线程,或者预先分配好多个进程或线程等待连接的请求.今 ...
- QT--TCP网络编程(客户端/服务器)
QT -----TCP网络编程 1.主要流程 1.客户端 创建QTcpSocket对象 连接到服务器 --connectToHost() 发送数据 ---write() 读取数据 ---readA ...
- Linux网络编程入门 (转载)
(一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...
- [转] - Linux网络编程 -- 网络知识介绍
(一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...
- 【转】Linux网络编程入门
(一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...
- 《转》Linux网络编程入门
原地址:http://www.cnblogs.com/duzouzhe/archive/2009/06/19/1506699.html (一)Linux网络编程--网络知识介绍 Linux网络编程-- ...
- Linux网络编程入门
(一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...
- C语言之网络编程(服务器和客户端)
Linux网络编程 1. 套接字:源IP地址和目的IP地址以及源端口号和目的端口号的组合称为套接字.其用于标识客户端请求的服务器和服务. 常用的TCP/IP协议的3种套接字类型如下所示. (1)流套接 ...
- Linux网络编程服务器模型选择之并发服务器(上)
与循环服务器的串行处理不同,并发服务器对服务请求并发处理.循环服务器只能够一个一个的处理客户端的请求,显然效率很低.并发服务器通过建立多个子进程来实现对请求的并发处理.并发服务器的一个难点是如何确定子 ...
- 服务器编程入门(4)Linux网络编程基础API
问题聚焦: 这节介绍的不仅是网络编程的几个API 更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系. 这节主要介绍三个方面的内容:套接字( ...
随机推荐
- SpringBoot 2.0中SpringWebContext 找不到无法使用的问题解决
为了应对在SpringBoot中的高并发及优化访问速度,我们一般会把页面上的数据查询出来,然后放到redis中进行缓存.减少数据库的压力. 在SpringBoot中一般使用 thymeleafView ...
- Android使用AchartEngine绘制曲线图
1.在布局文件中加入LinearLayout布局,如下: <LinearLayout android:id="@+id/chart" android:orientation= ...
- log4j.properties配置详情
log4j: log for java 是Apache的一个开源项目! 00.将我们的日志信息,输出到指定的位置(控制台 文件中) 01.我们可以控制每一条日志的输出格式 02.我们设置日志信息的 ...
- Kendo UI Widgets 概述
UI Widgets 概述 Kendo UI 是基于 jQuery 库开发的,Kendo UI widgets 是以 jQuery 插件形式提供的.这些插件的名称基本上都是以 kendo 作为前缀.比 ...
- JavaScript笔记6-数组新方法
七.ECMAScript5关于数组的新方法 1.forEach():遍历数组,并为每个元素调用传入的函数; 举例: var a = [1,2,3]; var sum = 0; //传一个 ...
- Linux 安装Memcache扩展支持
查看相关软件包 yum search memcached 安装memcache yum -y install memcachedMemcache关联php yum -y install php-pec ...
- 【虚拟机-可用性集】ARM 中可用性集使用的注意事项
Azure 目前有两种部署模型:经典部署模型 (ASM) 和资源管理器 (ARM).如果您之前使用过 ASM 模式下的可用性集,那么很可能在使用 ARM 模式下的可用性集时,会遇到一些问题或者疑惑.这 ...
- 首次将项目从svn下载到eclipse
1.点击 File --> Import,进入导入项目窗口 2.选择从SVN检出项目,点击Next 3.选择创建新的资源库位置,点击Next 4.在URL处输入SVN项目远程地址,点击Next ...
- SAP云平台的Document Service
SAP云平台以微服务的方式提供了Document的CRUD(增删改查)操作.该微服务基于标准的CMIS协议(Content Management Interoperability Service). ...
- UVA 11853 - Paintball 战场(dfs)
题意:有n个敌人,每个敌人有一个攻击范围,问你是否存在从西边到东边的路径,如果存在,输出入点和出点最靠北的坐标. 把每个敌人看出一个圆,从上往下跑dfs连通,如果到达底部,那么无解.要求出最靠北的坐标 ...