初等网络函数介绍(TCP)及示例程序
1、socket
int socket(int domain, int type,int protocol)
domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等)。 AF_UNIX只能够用于单一的Unix系统进程间
通信,而AF_INET是针对Internet的,因而可以允许在远程 主机之间通信(当我们 man socket时发现 domain可选项是 PF_*而不是AF_*,因为glibc是posix的实现 所以用PF代替了AF,不过我们都可以使用的)。
type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等) SOCK_STREAM表明我们用的是TCP协议,这样会提供按顺序的,可靠,双向,面向连接的比特流。 SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信。
protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了 socket为网络通讯做基本的准备。成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况。
2、bind
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd:是由socket调用返回的文件描述符。
addrlen:是sockaddr结构的长度。
my_addr:是一个指向sockaddr的指针。 在中有 sockaddr的定义
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};
不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替。在中有sockaddr_in的定义
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
我们主要使用Internet所以sin_family一般为AF_INET,sin_addr设置为INADDR_ANY表示可以 和任何的主机通信,sin_port是我们要监听的端口号。sin_zero[8]是用来填充的。 bind将本地的端口同socket返回的文件描述符捆绑在一起。成功是返回0,失败的情况和socket一样。
3、listen
int listen(int sockfd,int backlog)
sockfd:是bind后的文件描述符。
backlog:设置请求排队的最大长度。当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度。 listen函数将bind的文件描述符变为监听套接字。返回的情况和bind一样。
4、accept
int accept(int sockfd, struct sockaddr *addr,int *addrlen)
sockfd:是listen后的文件描述符。
addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了。 bind,listen和accept是服务器端用的函数,accept调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接。 accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了。 失败时返回-1.
5、connect
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
sockfd:socket返回的文件描述符。
serv_addr:储存了服务器端的连接信息。其中sin_add是服务端的地址
addrlen:serv_addr的长度
connect函数是客户端用来同服务端连接的。成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.
6、程序示例:
/******* 服务器程序 (server.c) ************/
#include “stdio.h”
#include “stdlib.h”
#include “errno.h”
#include “sys/types.h”
#include “sys/socket.h”
#include “unistd.h”
#include “netinet/in.h”
#include “netdb.h”
int main(int argc,char *argv[])
{
int sockfd,new_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_size,portnumber;
char hello[]=“Hello! Are You Fine? ”;
if ( argc!=2 ) {
fprintf(stderr,“Usage:%s portnumbera ”,argv[0]);
exit(1);
}
if((portnumber=atoi(argv[1]))<0) {
fprintf(stderr,“Usage:%s portnumbera ”,argv[0]);
exit(1);
}
/* 服务器端开始建立socket描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) {
fprintf(stderr,“Socket error:%s a”,strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr结构 */
bzero(&server_addr,sizeof(struct sockaddr_in));
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
server_addr.sin_port=htons(portnumber);
/* 捆绑sockfd描述符 */
if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1) {
fprintf(stderr,“Bind error:%s a”,strerror(errno));
exit(1);
}
/* 监听sockfd描述符 */
if(listen(sockfd,5)==-1) {
fprintf(stderr,“Listen error:%s a”,strerror(errno));
exit(1);
}
while(1) {
/* 服务器阻塞,直到客户程序建立连接 */
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1) {
fprintf(stderr,“Accept error:%s a”,strerror(errno));
exit(1);
}
fprintf(stderr,“Server get connection from %s ”,inet_ntoa(client_addr.sin_addr));
if(write(new_fd,hello,strlen(hello))==-1) {
fprintf(stderr,“Write Error:%s ”,strerror(errno));
exit(1);
}
close(new_fd);
}
close(sockfd);
exit(0);
}
/*******客户端程序 client.c*********/
#include “stdio.h”
#include “stdlib.h”
#include “errno.h”
#include “sys/types.h”
#include “sys/socket.h”
#include “unistd.h”
#include “netinet/in.h”
#include “netdb.h”
int main(int argc, char *argv[])
{
int sockfd;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent *host;
int portnumber,nbytes;
if(argc!=3) {
fprintf(stderr,“Usage:%s hostname portnumbera ”,argv[0]);
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL) {
fprintf(stderr,“Gethostname error ”);
exit(1);
}
if((portnumber=atoi(argv[2]))<0) {
fprintf(stderr,“Usage:%s hostname portnumbera ”,argv[0]);
exit(1);
}
/*客户程序开始建立 sockfd描述符*/
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) {
fprintf(stderr,“Socket Error:%sa ”,strerror(errno));
exit(1);
}
/*客户程序填充服务端的资料*/
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(portnumber);
server_addr.sin_addr=*((struct in_addr *)host->h_addr);
/*客户程序发起连接请求*/
if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1) {
fprintf(stderr,“Connect Error:%sa ”,strerror(errno));
exit(1);
}
if((nbytes=read(sockfd,buffer,1024))==-1) {
fprintf(stderr,“Read Error:%s ”,strerror(errno));
exit(1);
}
buffer[nbytes]='';
printf(“I have received:%s ”,buffer);
close(sockfd);
exit(0);
}
编译产生两个程序server.o(服务器端)和client.o(客户端) 先运行。/server.o portnumber& (portnumber随便取一个大于1204且不在/etc/services中出现的号码 就用8888好了),然后运行 ./client.o localhost 8888 看看有什么结果。
7、总结
总的来说网络程序是由两个部分组成的--客户端和服务器端。它们的建立步骤一般是:
服务器端
socket-->bind-->listen-->accept
服务套和客户机的信息函数
1、字节转换函数
在网络上面有着许多类型的机器,这些机器在表示数据的字节顺序是不同的, 比如i386芯片是低字节在内存地址的低端,高字节在高端,而alpha芯片却相反。 为了统一起来,在Linux下面,有专门的字节转换函数。
unsigned long int htonl(unsigned long int hostlong)
unsigned short int htons(unisgned short int hostshort)
unsigned long int ntohl(unsigned long int netlong)
unsigned short int ntohs(unsigned short int netshort)
在这四个转换函数中,h 代表host, n 代表 network.s 代表short l 代表long 第一个函数的意义是将本机器上的long数据转化为网络上的long. 其他几个函数的意义也差不多。
2、IP和域名的转换
在网络上标志一台机器可以用IP或者是用域名。那么我们怎么去进行转换呢?
struct hostent *gethostbyname(const char *hostname)
struct hostent *gethostbyaddr(const char *addr,int len,int type)
在中有struct hostent的定义
struct hostent{
char *h_name; /* 主机的正式名称 */
char *h_aliases; /* 主机的别名 */
int h_addrtype; /* 主机的地址类型 AF_INET*/
int h_length; /* 主机的地址长度 对于IP4 是4字节32位*/
char **h_addr_list; /* 主机的IP地址列表 */
}
#define h_addr h_addr_list[0] /* 主机的第一个IP地址*/
gethostbyname可以将机器名(如 linux.yessun.com)转换为一个结构指针。在这个结构里面储存了域名的信息
gethostbyaddr可以将一个32位的IP地址(C0A80001)转换为结构指针。
这两个函数失败时返回NULL 且设置h_errno错误变量,调用h_strerror()可以得到详细的出错信息
3、字符串的IP和32位的IP转换。
在网络上面我们用的IP都是数字加点(192.168.0.1)构成的, 而在struct in_addr结构中用的是32位的IP, 我们上面那个32位IP(C0A80001)是的192.168.0.1 为了转换我们可以使用下面两个函数
int inet_aton(const char *cp,struct in_addr *inp)
char *inet_ntoa(struct in_addr in)
函数里面 a 代表 ascii n 代表network.第一个函数表示将a.b.c.d的IP转换为32位的IP,存储在 inp指针里面。第二个是将32位IP转换为a.b.c.d的格式。
4、服务信息函数
在网络程序里面我们有时候需要知道端口。IP和服务信息。这个时候我们可以使用以下几个函数
int getsockname(int sockfd,struct sockaddr *localaddr,int *addrlen)
int getpeername(int sockfd,struct sockaddr *peeraddr, int *addrlen)
struct servent *getservbyname(const char *servname,const char *protoname)
struct servent *getservbyport(int port,const char *protoname)
struct servent {
char *s_name; /* 正式服务名 */
char **s_aliases; /* 别名列表 */
int s_port; /* 端口号 */
char *s_proto; /* 使用的协议 */
}
一般我们很少用这几个函数。对应客户端,当我们要得到连接的端口号时在connect调用成功后使用可得到 系统分配的端口号。对于服务端,我们用INADDR_ANY填充后,为了得到连接的IP我们可以在accept调用成功后 使用而得到IP地址。
在网络上有许多的默认端口和服务,比如端口21对ftp80对应WWW.为了得到指定的端口号的服务 我们可以调用第四个函数,相反为了得到端口号可以调用第三个函数。
5、示例程序
/******* …… (hostname_ip.c) ************/
#include “stdio.h”
#include “stdlib.h”
#include “errno.h”
#include “sys/types.h”
#include “sys/socket.h”
#include “unistd.h”
#include “netinet/in.h”
#include “netdb.h”
int main(int argc ,char **argv)
{
struct sockaddr_in addr;
struct hostent *host;
char **alias;
if(argc<2) {
fprintf(stderr,“Usage:%s hostname|ip a”,argv[0]);
exit(1);
}
argv++;
for(;*argv!=NULL;argv++) {
/* ……IP*/
if(inet_aton(*argv,&addr.sin_addr)!=0) {
host=gethostbyaddr((char *)&addr.sin_addr,4,AF_INET);
printf(“Address information of Ip %s ”,*argv);
}
else {
/* ,……?*/
host=gethostbyname(*argv);
printf(“Address information of host %s ”,*argv);
}
if(host==NULL) {
/* … ,……*/
fprintf(stderr,“No address information of %s ”,*argv);
continue;
}
printf(“Official host name %s ”,host->h_name);
printf(“Name aliases:”);
for(alias=host->h_aliases;*alias!=NULL;alias++)
printf(“%s ,”,*alias);
printf(“ Ip address:”);
for(alias=host->h_addr_list;*alias!=NULL;alias++)
printf(“%s ,”,inet_ntoa(*(struct in_addr *)(*alias)));
}
1.套接口的地址结构确保了进程与内核之间的通讯
2.套接口的地址结构中保存地址值的是二进制,inet_pton ,inet_ntop 函数用于地址的文本表达式和二进制之间的转换
3.每个协议族都有自己的地址结构 以sockaddr_开头,后面加协议族后缀,
网际协议族的地址结构sockaddr_in
4.结构不参与通讯
5.进程到内核的函数,内核到进程的函数
6.字节排序函数来由:不同系统字节排序方式不一样,大端或小端,
套接口地址结构中某些成员按网络字节序维护,所以需要转换
htons,htonl,ntohs,ntohl
7.确定主机字节序程序
用到union:共用体,共用体的几个变量公用一个内存位置,不同时间可以保存不同的值,同一时间只能存储一个成员变量的值,共用体内存空间是其成员最大的类型空间,但其成员不能为静态;
8.字节操纵函数
处理的不是字符串,而是字节;
第一组函数bzero,bcmp,bcopy----》b代表byte,入参为const的指针,表示函数不可以修改指针所指内存
第二组函数memset,memcpy,memcmp--》m表示memory,所有memXXX的函数,第三个参数为结构大小,size_t
9.点分十进制到网络二进制转换函数
1)几乎废弃的函数inet_aton,inet_addr,inet_ntoa
inet_addr 无法处理255.255.255.255,因为2^32不包括
2)inet_pton,inet_ntop
p 地址表达式 presentation ascii串,n 数值numeric 二进制
inet_pton ipv4的实现 ,用到了inet_aton函数
inet_ntop ipv4的实现
宏定义INET_ADDRSTRLEN 16,INET6_ADDRSTRLEN 46
用于保存 网络二进制地址转换为点分十进制地址,数组的大小
10.sock_ntop函数
什么是对结果静态的存储?为什么阻碍了函数的课重入与线程安全
为了ipv4与ipv6间移植
11.readn,writen,readline的实现
使用readn,writen,readline的原因:在字节流套接口(sock_stream)上读或写的时候,内核的缓冲区达到极限的时候,此时则需要再次调用read或write函数读出或输入剩余的字节
新版的readline,跟踪一下
12.issocktype函数:测试一个描述字是否某给定类型----》这种应用用于一个由另外一个程序exec执行的程序中
其实现用到了fstat函数
完整的读写函数
一旦我们建立了连接,我们的下一步就是进行通信了。在Linux下面把我们前面建立的通道 看成是文件描述符,这样服务器端和客户端进行通信时候,只要往文件描述符里面读写东西了。 就象我们往文件读写一样。
4.1 写函数write
ssize_t write(int fd,const void *buf,size_t nbytes)
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1. 并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。
1)write的返回值大于0,表示写了部分或者是全部的数据。
2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。
如果错误为EINTR表示在写的时候出现了中断错误。
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
为了处理以上的情况,我们自己编写一个写函数来处理这几种情况。
int my_write(int fd,void *buffer,int length)
{
int bytes_left;
int written_bytes;
char *ptr;
ptr=buffer;
bytes_left=length;
while(bytes_left>0)
{
/* 开始写*/
written_bytes=write(fd,ptr,bytes_left);
if(written_bytes<=0) /* 出错了*/
{
if(errno==EINTR) /* 中断错误 我们继续写*/
written_bytes=0;
else /* 其他错误 没有办法,只好撤退了*/
return(-1);
}
bytes_left-=written_bytes;
ptr+=written_bytes; /* 从剩下的地方继续写 */
}
return(0);
}
4.2 读函数read
ssize_t read(int fd,void *buf,size_t nbyte) read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的, 如果是ECONNREST表示网络连接出了问题。 和上面一样,我们也写一个自己的读函数。
int my_read(int fd,void *buffer,int length)
{
int bytes_left;
int bytes_read;
char *ptr;
bytes_left=length;
while(bytes_left>0)
{
bytes_read=read(fd,ptr,bytes_read);
if(bytes_read<0)
{
if(errno==EINTR)
bytes_read=0;
else
return(-1);
}
else if(bytes_read==0)
break;
bytes_left-=bytes_read;
ptr+=bytes_read;
}
return(length-bytes_left);
}
4.3 数据的传递
有了上面的两个函数,我们就可以向客户端或者是服务端传递数据了。比如我们要传递一个结构。可以使用如下方式
/* 客户端向服务端写 */
struct my_struct my_struct_client;
write(fd,(void *)&my_struct_client,sizeof(struct my_struct);
/* 服务端的读*/
char buffer[sizeof(struct my_struct)];
struct *my_struct_server;
read(fd,(void *)buffer,sizeof(struct my_struct));
my_struct_server=(struct my_struct *)buffer;
在网络上传递数据时我们一般都是把数据转化为char类型的数据传递。接收的时候也是一样的 注意的是我们没有必要在网络上传递指针(因为传递指针是没有任何意义的,我们必须传递指针所指向的内容)
今天,开始在工作之余,抽出时间来学习linux界面编程。我计划在未来的半年内,学会 linux下的GTK+界面编程。这个文档作为我学习linux编程的第一个文档,我会尽量做的详 细一点。我所用的linux版本是Red Hat 9.0。
第一个程序——Hello,World
在开始写第一个程序之前,需要做一点基本操作。
我登陆linux使用的是root用户(根用户,也叫超级用户)。所以,所有笔记中产生的截图,均是root用户下的。
一、在linux桌面点击右键,在快捷菜单中选择“新建终端”选项,打开类似DOS的窗口。
二、使用mkdir命令创建一个文件夹,文件夹命名为gtkgui,命令输入为:mkdir gtkgui
输入完毕后敲回车键即可创建目录gtkgui,需要注意的是该目录的路径是:/root/gtktui
三、使用cd命令进入目录gtkgui 命令输入:cd gtkgui
输入命令后敲回车键即可进入目录gtkgui
四、创建第一个程序存放的目录,和第一步一样,这次在gtkgui目录下创建一个子目录1gui 然后进入1gui目录。创建目录命令:mkdir 1gui 进入目录命令:cd lgui 命令界面输入。
(注意1gui第一个字符是“一”,不是小写的L)。
小提示:若你想查看当前目录的绝对路径。可以在终端敲入命令:pwd
现在可以清楚的看到当前工作目录是“/root/gtkgui/1gui”。
五、编写代码,在开始写第一个程序之前,说说VI编辑器。假设要写的程序是firstgui.c,则在终端中敲入 vi firstgui.c 然后回车,就进入编辑文件firstgui.c状态了,VI命令后面的文件若存在,则直接打开,若不存在,则新建一个。进入文件编辑状态后,必须按a键, 才可向文件输入东西,当编辑完后,按esc键退出编辑模式,再按shift+;键,最后输入wq(write and quit)退出 VI。好了,在写程序中,再说一遍就记住了。
1、 创建 firstgui.c并编辑它
命令:vi first.c 然后敲回车进入编辑模式
必须先按下 a 键(还有其他的键也可(如i键),不过我觉得按下a键比较方便)才可输入字符,现在按一下a键,终端变成图1.8所示的效果。
按一下a键后(注意左下方有个--插入--),进入编辑模式
现在开始输入第一个程序的代码,代码原型如下:
#include <gtk/gtk.h>
int main(int argc,char*argv[])
{
GtkWidget *window;
gtk_init(&argc,&argv);
window=gtk_window_new(GKT_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window),“Hello,World”);
gtk_widget_show(window);
gtk_main();
return 0;
}
现在代码输入完毕,需要保存文件并推出编辑器,先按一下esc键退出编辑模式,然后按组合键:shift+;键 最后输入wq并敲回车键就保存了文件并退出编辑模式了。退出VI后的界面如图1.20所示。
使用ll命令(小写的LL)查看刚才编写的firstgui.c是否存在。
若想看文件内容,使用cat firstgui.c 命令就可看到文件firstgui.c的内容了。
六、编译代码
在终端敲入下列命令及参数,编译连接程序
命令及参数:gcc firstgui.c -o firstgui `pkg-config --cflags --libs gtk+-2.0`
命令说明: gcc 编译命令
firstgui.c 源程序文件名
-o 编译参数,(不是0(零),是opq的o)
firstgui 编译连接后的可执行文件名,可以为任意合法的文件名
` 这个符号是和~呆在一起的那个键上,不要误认为是单引号‘
pkg后面与-config之间没有空格
cflags 前面是两个短线 --
libs前面也是两个短线 --
七、运行程序
键入 ./firstgui 后回车便可看到效果了。
需要注意的是,该程序当你点击窗口右上角上的小X关闭窗口时,窗口倒是关闭了。但是程序还在运行,你必须在终端下按组合键:Ctrl+c来关闭进程。
八、程序分析
GtkWidget *window; 相当于Windows下的
WNDCLASS
结构体,不过window在此只是一个窗口指针,用于指向函数gtk_window_new()函数返回的窗口地址而已。
gtk_init(&argc,&argv);
初始化界面库等一系列操作的函数,当其调用失败时,其将终止我们的程序,目前只要记住在编写任何窗口函数之前,都保证该函数是第一个被调用的就行了。
window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
该函数创建一个新的GtkWidget对象(目前我认为其就是创建了一个还没有显示的窗 口),并返回该对象的指针。该函数参数仅有两个,其中一个是上面的GTK_WINDOW_TOPLEVEL,表示创建一个“主窗口”,另一个则是 GTK_WINDOW_POPUP,表示创建一个弹出窗口。
gtk_window_set_title(GTK_WINDOW(window),“Hello,World”);该函数设置窗口的标题。
gtk_widget_show(window); 该函数将窗口显示出来。
gtk_main();该函数捕获一些消息吧。在没有调用函数gtk_main_quit()函数之前,即使你关闭窗口,进程也不关闭的原因就在此。
九、总结
创建窗口主要步骤:
1、GtkWidget *window;
2、gtk_init(&argc,&argv);
3、windows=gtk_window_new(GTK_WINDOW_TOPLEVEL);
4、gtk_widget_show(window);
5、gtk_main();
最后,注意编译命令。后面的那些参数。
目前,X窗口(Xwindow)和GNU编译系统已成为应用linux或unix操作系统的计算机工作站和大型计算机上最主要的图形用户界面系统。而GTK正是两者结合的编程开发包,它比以往用的Xwindow/Motif编程更为简单方便,功能也很强大。所以,我想会有着较好的应用前景。这是我的心得体会,希望能够对大家有所帮助!