Linux之Socket编程
阅读原文时间:2023年07月10日阅读:2

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),socket就提供了这些操作对应的函数接口。

socket可以看成是用户进程与内核网络协议栈的编程接口。

socket不仅可以用于本机的进程间通信,还可以 用于网络上不同主机的进程间通信。

IPV4套妾口地址结构通常也弥为“网际套接字地址结构”,它以 “sockaddr_in”命名,定义在头文件

struct sockaddr_in{
uint8_t sin_len; //整个sockaddr_in结构体的长度
sa_family_t sin_family; //指定该地址家族,在这里必须设为AF_INET
in_port_t sin_port; //端口(2字节)
struct in_addr sin_addr; //IPv4的地址(4字节)
char sin_zero[]; //暂不使用,一般将其设置为0 (8字节)
}

IPv4套接字一般只需关心3个字段

struct sockaddr_in {
sa familytsin_family;/* address family:AF_INET */
in_portt 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 hetwork byte order
}

通用地址结构用来指定与套接字关联的地址

struct sockaddr{
uint8_t sin_len; //整个sockaddr结构体的关度
sa_ family_t sin_family; //指定该地址家族
char sa_data[]; //由sin_family决定它的形式
}

因为不同的协议地址结构形式可能不一样,通用的可以用于任何协议的接口

一般将IPv4的sockaddr_in强行转换为通用的地址结构sockaddr

字节序

大端字节序(Big Endian) :最高有效位(MSB:Most Significant Bit)存储于最低内存地址 处,最低有效位(LSB:Lowest Significant Bit;存储于最高内存地坨处。

小端字节序(l.ittle ndian) :最高有效位(MSB:Most Significant Bit)存储于最高内存地址 处,最低有效位(LSB:Lowest Significant Bit>存储于最低内存地垃处。

主机字节序

不同的主有不同的字节序,如:86为小端字节序,Motorola 6800为 大端字节序,ARM字节序是可配置的。

网络字节序

网络字节序规定为大端字节序

为了将字节序统一,就出现了网络字节序,为大端字节序

编写一个程序测试大小端

#include
int main(void)
{
unsigned int x=0x12345678;
unsigned char *p=(unsigned char*)&x;
printf("%0x %0x %0x %0x\n",p[],p[],p[],p[]);
return ;
}

Linux下运行结果:

说明是小端模式

uint32_t htonl(uint32_t hostlong); //主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong); ////网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort);

说明:在上述的函数中

h代表 host

n代表 network

s代表 short

I代表 long

程序测试网络字节序

#include
#include
int main()
{
unsigned int x = 0x12345678;
unsigned int y=htonl(x);
unsigned char *p=(unsigned char*)&y;
printf("%0x %0x %0x %0x\n",p[],p[],p[],p[]);
return ;
}

运行结果:

可见,网络字节序会将本地字节序转换为大端模式

#include
#include

int inet_ aton(const char *cp,struct in_addr *inp);//将点分十进制转换为网络字节序的结构
in_addr_t inet_addr(const char *cp); //将点分十进制IP地址转换为32位整数
char *inet_ntoa(struct in_addr in); //将网络字节序的结构转换为点分十进制

一般的地址:点分十进制形式,如:192.168.0.100

测试程序:

int main()
{
unsigned long addr=inet_addr("192.168.0.100");
cout<<ntohl(addr)<<endl;
return ;
}
//运行结果:3232235620

int main()
{
unsigned long addr=inet_addr("192.168.0.100");
struct in_addr ipaddr;
ipaddr.s_addr=addr;
cout<<inet_ntoa(ipaddr)<<endl;
return ;
}
//运行结果:192.168.0.100

常用的三种:

流式套接字(SOCK_STREAM)  (TCP协议)

提供面向连接的、可靠的数据传输服务,数据无 差错,无重复的发送,且按发送顺序接收。

数据报式套接字(SOCK_DGRAM) (UDP协议)

提供无连接服务。不提供无错保证,数据可能丢 失或重复,并且接收顺序混乱。

原始套接字(SOCK_RAW)

可以将应用层直接封装成IP层能认识的协议格式


头文件

功能:创建一个套接字用于通信

原型

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

参数

domain:指定通信协议族(protocol family)

   Name                Purpose                          Man page  
   AF\_UNIX, AF\_LOCAL   Local communication              unix()  
   AF\_INET             IPv4 Internet protocols          ip()  
   AF\_INET6            IPv6 Internet protocols          ipv6()  
   AF\_IPX              IPX - Novell protocols  
   AF\_NETLINK          Kernel user interface device     netlink()  
   AF\_X25              ITU-T X. / ISO- protocol   x25()  
   AF\_AX25             Amateur radio AX. protocol  
   AF\_ATMPVC           Access to raw ATM PVCs  
   AF\_APPLETALK        Appletalk                        ddp()  
   AF\_PACKET           Low level packet interface       packet()

type:指定socket类型,流式套接字SOCK_STREAM,数据报套 接字SOCK DGRAM,原始套接字SOCK RAW

   SOCK\_STREAM     Provides  sequenced,  reliable,  two-way,  connection-based  
                   byte  streams.   An out-of-band data transmission mechanism  
                   may be supported.

   SOCK\_DGRAM      Supports datagrams (connectionless, unreliable messages  of  
                   a fixed maximum length).

   SOCK\_SEQPACKET  Provides  a  sequenced,  reliable, two-way connection-based  
                   data transmission  path  for  datagrams  of  fixed  maximum  
                   length;  a  consumer  is  required to read an entire packet  
                   with each input system call.

   SOCK\_RAW        Provides raw network protocol access.

   SOCK\_RDM        Provides a reliable datagram layer that does not  guarantee  
                   ordering.

   SOCK\_PACKET     Obsolete  and  should  not  be  used  in  new programs; see  
                   packet().  
   SOCK\_NONBLOCK   Set  the  O\_NONBLOCK  file status flag on the new open file  
                   description.  Using this flag saves extra calls to fcntl()  
                   to achieve the same result.

   SOCK\_CLOEXEC    Set  the  close-on-exec  (FD\_CLOEXEC)  flag on the new file  
                   descriptor.  See the description of the O\_CLOEXEC  flag  in  
                   open() for reasons why this may be useful.

protocol:协议类型

返回值:成功返回非负整数,它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1

功能:绑定一个本地地址到套接字

原型

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

参数

sockfd:socket函数返回的套接字

addr:要绑定的地址

addrlen:地址长度

返回值:成功返回0,失败返回-1

功能:将套接字用于监听进入的连接

原型

int listen (int sockfd,int backlog);

参数

sockfd: socket函数返回的套接字口

backlog: 规定内核为此套接字排队的最大连妾个数口,表示已完成队列和未完成队列的组合,未完成队列表示三次握手还没有成功的条目

返回值:成功返回0,失败返回-1,规定了并发连接的数目

一般来说,listen函数应该在调用socket和bind函数之后,调用函数accept之前调用。

对于给定的监听套接口,内核要维护两个队列:

1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程

2、已完成连接的队列

// 调用listen函数后,就成了被动套接字,否则是主动套接字

// 主动套接字:发送连接(connect)

// 被动套接字:接收连接(accept)

功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。

原型

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

sockfd:服务器套接字

addr:将返回对等方的套接字地址,相当于把对方的信息填充到结构体中

addrlen:返回对等方的套接字地址长度

返回值:成功返回非负整数,失败返回-1

功能:建立一个连接至addr所指定的套接字

原型

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

参数

sockfd:未连接套接字

addr:要连接的套接字地址

addrlen:第二个参数addr长度

返回值:成功返回0,失败返回-1

服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作,即实现了网咯中不同进程之间的通信.网络I/O操作有下面几组:

#include

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

#include
#include

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);

推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include
int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。


回射服务器代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;

#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
} while ();

int main(void)
{
//socket
int listenfd;
//listenfd=socket(PF_INET,SOCK_STREAM,0);
if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<)
{
ERR_EXIT("socket");
}

//填充地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr,,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons();  
servaddr.sin\_addr.s\_addr=htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
//servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");  
//inet\_aton("127.0.0.1",&servaddr.sin\_addr);

//地址复用  
int on=;  
if(setsockopt(listenfd,SOL\_SOCKET,SO\_REUSEADDR,&on,sizeof(on))<)  
{  
    ERR\_EXIT("setsocketopt");  
}

//bind 绑定listenfd和本地地址结构  
if(bind(listenfd,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
{  
    ERR\_EXIT("bind");  
}

if(listen(listenfd,SOMAXCONN)<)  
{  
    ERR\_EXIT("listen");  
}

// 调用listen函数后,就成了被动套接字,否则是主动套接字  
// 主动套接字:发送连接(connect)  
// 被动套接字:接收连接(accept)

//对方的地址  
struct sockaddr\_in peeraddr;  
socklen\_t peerlen=sizeof(peeraddr);  
int conn;   //已连接套接字(主动)  
if((conn=accept(listenfd,(struct sockaddr\*)&peeraddr,&peerlen))<)  
{  
    ERR\_EXIT("accept");  
}  
//连接成功后打印客户端的ip和端口  
printf("client: ip=%s | port=%d\\n",inet\_ntoa(peeraddr.sin\_addr),ntohs(peeraddr.sin\_port));

char recvbuf\[\];  
while ()  
{  
    memset(recvbuf, , sizeof(recvbuf));

    int ret=read(conn,recvbuf,sizeof(recvbuf));  
    fputs(recvbuf,stdout);  
    write(conn,recvbuf,ret);  
}

//关闭套接口  
close(conn);  
close(listenfd);  
return ;  

}

回射客户端代码

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while ();

int main()
{
//socket
int sock;
if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<)
{
ERR_EXIT("socket");
}

struct sockaddr\_in cliaddr;  
memset(&cliaddr, , sizeof(cliaddr));  
cliaddr.sin\_family = AF\_INET;  
cliaddr.sin\_port = htons();  
cliaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
if(bind(sock,(struct sockaddr\*)&cliaddr,sizeof(cliaddr))<)  
{  
    ERR\_EXIT("bind");  
}

//指定服务器的地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr,,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons();  
servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");

//客户端不需要绑定和监听  
//connect 用本地套接字连接服务器的地址  
if(connect(sock,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
    ERR\_EXIT("connect");

char sendbuf\[\]={};  
char recvbuf\[\]={};  
while (fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)  
{  
    write(sock,sendbuf,strlen(sendbuf));  
    read(sock,recvbuf,sizeof(recvbuf));  
    fputs(recvbuf,stdout);  
    memset(sendbuf,,sizeof(sendbuf));  
    memset(recvbuf,,sizeof(recvbuf));  
}

//关闭套接字  
close(sock);

return ;  

}

客户端的套接字sock和服务器的套接字conn构成连接,两个套接字都有自己的地址

conn是在绑定的时候确定的

sock实在连接成功的时候确定的


客户端一般不需要绑定端口,如果不绑定,一般是随机的端口,但是也可以为其绑定固定的端口

//socket
int sock;
if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<)
{
ERR_EXIT("socket");
}

//指定客户端的地址结构  
struct sockaddr\_in cliaddr;  
memset(&cliaddr, , sizeof(cliaddr));  
cliaddr.sin\_family = AF\_INET;  
cliaddr.sin\_port = htons();  
cliaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
if(bind(sock,(struct sockaddr\*)&cliaddr,sizeof(cliaddr))<)  
{  
    ERR\_EXIT("bind");  
}

//指定服务器的地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr,,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons();  
servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");

//客户端不需要绑定和监听  
//connect 用本地套接字连接服务器的地址  
if(connect(sock,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
    ERR\_EXIT("connect");

在服务器端接收客户端后,打印出客户端的端口

if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<)
{
ERR_EXIT("accept");
}
//连接成功后打印客户端的ip和端口
printf("client: ip=%s | port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

结果是:

服务器在重启后,在重新运行时绑定会报地址已经使用,因为现在服务器处于TIME_WAIT状态

这个状态下,不能再绑定客户端,需要等待一段时间。

解决办法:地址复用

服务器端尽可能使用REUSEADDR

绑定之前尽可能调用setsockopt来设置REUSEADDR套接字选项。

使用REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器。

使用REUSEADDR可以在TIME_WAIT状态还没有消失的时候,就允许重启

//地址复用
int on=;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<)
{
ERR_EXIT("setsocketopt");
}

//bind 绑定listenfd和本地地址结构  
if(bind(listenfd,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
{  
    ERR\_EXIT("bind");  
}

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
//////////////////////////////////////////
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
} while ();

void do_service(int conn)
{
char recvbuf[];
while ()
{
memset(recvbuf, , sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
if(ret==) //客户端关闭了
{
printf("client_close!");
break;
}
else if(ret==-)
{
ERR_EXIT("read");
}
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
}

///////////////////////////////////////////////////

int main(void)
{
//socket
int listenfd;
//listenfd=socket(PF_INET,SOCK_STREAM,0);
if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<)
{
ERR_EXIT("socket");
}

//填充地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr,,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons();  
servaddr.sin\_addr.s\_addr=htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
//servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");  
//inet\_aton("127.0.0.1",&servaddr.sin\_addr);

//地址复用  
int on=;  
if(setsockopt(listenfd,SOL\_SOCKET,SO\_REUSEADDR,&on,sizeof(on))<)  
{  
    ERR\_EXIT("setsocketopt");  
}

//bind 绑定listenfd和本地地址结构  
if(bind(listenfd,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
{  
    ERR\_EXIT("bind");  
}

if(listen(listenfd,SOMAXCONN)<)  
{  
    ERR\_EXIT("listen");  
}

// 调用listen函数后,就成了被动套接字,否则是主动套接字  
// 主动套接字:发送连接(connect)  
// 被动套接字:接收连接(accept)

//对方的地址  
struct sockaddr\_in peeraddr;  
socklen\_t peerlen=sizeof(peeraddr);  
int conn;   //已连接套接字(主动)

pid\_t pid;  
while ()  
{  
    if((conn=accept(listenfd,(struct sockaddr\*)&peeraddr,&peerlen))<)  
    {  
        ERR\_EXIT("accept");  
    }  
    //连接成功后打印客户端的ip和端口  
    printf("client: ip=%s | port=%d\\n",inet\_ntoa(peeraddr.sin\_addr),ntohs(peeraddr.sin\_port));

    pid=fork();  
    if(pid==-)  
    {  
        ERR\_EXIT("fork");  
    }  
    if (pid==)  
    {  
        /\* 子进程的处理 \*/  
        close(listenfd);    //子进程不需要处理监听,子进程处理通信细节  
        //通信处理封装函数  
        do\_service(conn);  
        //一旦客户端关闭进程返回了,这个子进程就要结束  
        exit(EXIT\_SUCCESS);  
    }  
    else  
    {  
        /\*父进程的处理 \*/  
        close(conn);    //父进程不需要处理连接  
    }  
}  
return ;  

}

一个连接对应一个进程来并发处理

服务器有两种套接字,

1.监听套接字:处理三次握手,一旦三次握手创建完成,就将其放在已连接队列中,accept就可以从队列中返回一个连接

2.已连接套接字:accept返回的套接字,主要用来通信,并不能用来接受连接

服务器

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

using namespace std;

#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while ();

//信号处理函数
void handler(int sig)
{
printf("recv a sig=%d\n",sig);
exit(EXIT_SUCCESS);
}

int main(void)
{
//socket
int listenfd;
//listenfd=socket(PF_INET,SOCK_STREAM,0);
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
{
ERR_EXIT("socket");
}

//填充地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr, , sizeof(servaddr));  
servaddr.sin\_family = AF\_INET;  
servaddr.sin\_port = htons();  
servaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
//servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");  
//inet\_aton("127.0.0.1",&servaddr.sin\_addr);

//地址复用  
int on = ;  
if (setsockopt(listenfd, SOL\_SOCKET, SO\_REUSEADDR, &on, sizeof(on)) < )  
{  
    ERR\_EXIT("setsocketopt");  
}

//bind 绑定listenfd和本地地址结构  
if (bind(listenfd, (struct sockaddr \*)&servaddr, sizeof(servaddr)) < )  
{  
    ERR\_EXIT("bind");  
}

if (listen(listenfd, SOMAXCONN) < )  
{  
    ERR\_EXIT("listen");  
}

// 调用listen函数后,就成了被动套接字,否则是主动套接字  
// 主动套接字:发送连接(connect)  
// 被动套接字:接收连接(accept)

//对方的地址  
struct sockaddr\_in peeraddr;  
socklen\_t peerlen = sizeof(peeraddr);  
int conn; //已连接套接字(主动)  
if ((conn = accept(listenfd, (struct sockaddr \*)&peeraddr, &peerlen)) < )  
{  
    ERR\_EXIT("accept");  
}  
//连接成功后打印客户端的ip和端口  
printf("client: ip=%s | port=%d\\n", inet\_ntoa(peeraddr.sin\_addr), ntohs(peeraddr.sin\_port));

pid\_t pid;  
pid=fork();  
if(pid==-)  
{  
    ERR\_EXIT("fork");  
}  
if(pid==)  //子进程,发送数据  
{  
    signal(SIGUSR1,handler);    //handler是受到信号后的处理函数  
    char sendbuf\[\];  
    while (fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)  
    {  
        write(conn,sendbuf,strlen(sendbuf));  
        memset(sendbuf,,sizeof(sendbuf));  
    }  
    exit(EXIT\_SUCCESS);  
}  
else    //父进程,接受数据  
{  
    char recvbuf\[\];  
    while ()  
    {  
        memset(recvbuf, , sizeof(recvbuf));  
        int ret = read(conn, recvbuf, sizeof(recvbuf));  
        if(ret==-) //读取失败  
        {  
            ERR\_EXIT("read");  
        }  
        if(ret==)  //对方关闭  
        {  
            printf ("peer close\\n");  
            break;  
        }  
        fputs(recvbuf, stdout);  
    }  
    //父进程退出时,向子进程发送kill信号  
    kill(pid,SIGUSR1);  //父进程得到的pid是子进程的pid,子进程得到的pid为0  
    exit(EXIT\_SUCCESS);  
}  
return ;  

}

客户端

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while ();

//信号处理函数
void handler(int sig)
{
printf("recv a sig=%d\n", sig);
exit(EXIT_SUCCESS);
}

int main()
{
//socket
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
{
ERR_EXIT("socket");
}

// struct sockaddr\_in cliaddr;  
// memset(&cliaddr, 0, sizeof(cliaddr));  
// cliaddr.sin\_family = AF\_INET;  
// cliaddr.sin\_port = htons(2019);  
// cliaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
// if (bind(sock, (struct sockaddr \*)&cliaddr, sizeof(cliaddr)) < 0)  
// {  
//     ERR\_EXIT("bind");  
// }

//指定服务器的地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr, , sizeof(servaddr));  
servaddr.sin\_family = AF\_INET;  
servaddr.sin\_port = htons();  
servaddr.sin\_addr.s\_addr = inet\_addr("127.0.0.1");

//客户端不需要绑定和监听  
//connect 用本地套接字连接服务器的地址  
if (connect(sock, (struct sockaddr \*)&servaddr, sizeof(servaddr)) < )  
    ERR\_EXIT("connect");

pid\_t pid;  
pid=fork();  
if(pid==-)  
{  
    ERR\_EXIT("fork");  
}  
if (pid==) //子进程,接受数据  
{  
    char recvbuf\[\];  
    while ()  
    {  
        memset(recvbuf,,sizeof(recvbuf));  
        int ret=read(sock,recvbuf,sizeof(recvbuf));  
        if(ret==-)  
        {  
            ERR\_EXIT("read");  
        }  
        else if(ret==)  
        {  
            printf("peer close\\n");  
            break;  
        }  
        fputs(recvbuf,stdout);  
    }  
    close(sock);  
    kill(getppid(),SIGUSR1);    //杀掉父进程  
}  
else    //父进程,发送数据  
{  
    signal(SIGUSR1,handler);  
    char sendbuf\[\]={};  
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)  
    {  
        write(sock, sendbuf, strlen(sendbuf));  
        memset(sendbuf, , sizeof(sendbuf));  
    }  
    close(sock);  
}  
return ;  

}

我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

  • 客户端向服务器发送一个SYN J
  • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
  • 客户端再想服务器发一个确认ACK K+1

只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次挥手释放连接的过程,请看下图:

图示过程如下:

  • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
  • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
  • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
  • 接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

TCP 字节流,无边界,对于对等方来说,不能保证一次读操作返回的是一个消息还是多个消息,存在粘包问题

UDP 报文消息,有边界,能保证对等方,一次读操作返回的是一个消息

粘包产生的原因

1.应用层缓冲区大小超过了套接口发送的缓冲区,消息被分隔

2.TCP传输有最大MSS的限制,可能产生分隔

3.如果传输的大小超过了MTU的限制,会在ip层进行分割

4.其他:流量控制,拥塞控制等

readn

ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nread = read(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nread == )  
        return count - nleft;

    bufp += nread;  
    nleft -= nread;  
}

return count;  

}

writen

ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nwritten = write(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nwritten == )  
        continue;

    bufp += nwritten;  
    nleft -= nwritten;  
}

return count;  

}

readn和wirten都是以定长包的形式发送,但是不一定每次都要发送的实际数据报这么长的字节,就增加了网络的负担

改进:可以自定义网络协议的包,定义一个包结构体,存储数据的长度和数据

本质上是要在应用层维护消息与消息的边界

定长包

包尾加\r\n(ftp)(本来就有,就无法区分)

包头加上包体长度(先接受包头的长度,再根据包头接受包体的长度)

更复杂的应用层协议

服务器

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include
using namespace std;
//////////////////////////////////////////
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
} while ();

//自定义包结构体
struct packet
{
int len; //存放数据的实际长度
char buf[];
};

ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nread = read(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nread == )  
        return count - nleft;

    bufp += nread;  
    nleft -= nread;  
}

return count;  

}

ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nwritten = write(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nwritten == )  
        continue;

    bufp += nwritten;  
    nleft -= nwritten;  
}

return count;  

}

void do_service(int conn)
{
struct packet recvbuf;
int n; //包的长度
while ()
{
memset(&recvbuf, , sizeof(recvbuf));
int ret = readn(conn, &recvbuf.len, ); //先接受4个字节
if (ret == -)
{
ERR_EXIT("read");
}
if (ret <) //客户端关闭了
{
printf("client_close!");
break;
}
n = htonl(recvbuf.len);
ret = readn(conn, recvbuf.buf, n);
if (ret == -)
{
ERR_EXIT("read");
}
if (ret<n) //客户端关闭了
{
printf("client_close!");
break;
}

    fputs(recvbuf.buf, stdout);  
    writen(conn, &recvbuf, +n);  
}  

}
///////////////////////////////////////////////////

int main(void)
{
//socket
int listenfd;
//listenfd=socket(PF_INET,SOCK_STREAM,0);
if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<)
{
ERR_EXIT("socket");
}

//填充地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr,,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons();  
servaddr.sin\_addr.s\_addr=htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
//servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");  
//inet\_aton("127.0.0.1",&servaddr.sin\_addr);

//地址复用  
int on=;  
if(setsockopt(listenfd,SOL\_SOCKET,SO\_REUSEADDR,&on,sizeof(on))<)  
{  
    ERR\_EXIT("setsocketopt");  
}

//bind 绑定listenfd和本地地址结构  
if(bind(listenfd,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
{  
    ERR\_EXIT("bind");  
}

if(listen(listenfd,SOMAXCONN)<)  
{  
    ERR\_EXIT("listen");  
}

// 调用listen函数后,就成了被动套接字,否则是主动套接字  
// 主动套接字:发送连接(connect)  
// 被动套接字:接收连接(accept)

//对方的地址  
struct sockaddr\_in peeraddr;  
socklen\_t peerlen=sizeof(peeraddr);  
int conn;   //已连接套接字(主动)

pid\_t pid;  
while ()  
{  
    if((conn=accept(listenfd,(struct sockaddr\*)&peeraddr,&peerlen))<)  
    {  
        ERR\_EXIT("accept");  
    }  
    //连接成功后打印客户端的ip和端口  
    printf("client: ip=%s | port=%d\\n",inet\_ntoa(peeraddr.sin\_addr),ntohs(peeraddr.sin\_port));

    pid=fork();  
    if(pid==-)  
    {  
        ERR\_EXIT("fork");  
    }  
    if (pid==)  
    {  
        /\* 子进程的处理 \*/  
        close(listenfd);    //子进程不需要处理监听,子进程处理通信细节  
        //通信处理封装函数  
        do\_service(conn);  
        //一旦客户端关闭进程返回了,这个子进程就要结束  
        exit(EXIT\_SUCCESS);  
    }  
    else  
    {  
        /\*父进程的处理 \*/  
        close(conn);    //父进程不需要处理连接  
    }  
}  
return ;  

}

客户端

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//////////////////////////////////////////////////////////////
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while ();

//自定义包结构体
struct packet
{
int len; //存放数据的实际长度
char buf[];
};

ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nread = read(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nread == )  
        return count - nleft;

    bufp += nread;  
    nleft -= nread;  
}

return count;  

}

ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nwritten = write(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nwritten == )  
        continue;

    bufp += nwritten;  
    nleft -= nwritten;  
}

return count;  

}

/////////////////////////////////////////////////////////////////
int main()
{
//socket
int sock;
if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<)
{
ERR_EXIT("socket");
}

// struct sockaddr\_in cliaddr;  
// memset(&cliaddr, 0, sizeof(cliaddr));  
// cliaddr.sin\_family = AF\_INET;  
// cliaddr.sin\_port = htons(2019);  
// cliaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
// if(bind(sock,(struct sockaddr\*)&cliaddr,sizeof(cliaddr))<0)  
// {  
//     ERR\_EXIT("bind");  
// }

//指定服务器的地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr,,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons();  
servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");

//客户端不需要绑定和监听  
//connect 用本地套接字连接服务器的地址  
if(connect(sock,(struct sockaddr\*)&servaddr,sizeof(servaddr))<)  
    ERR\_EXIT("connect");

struct packet sendbuf;  
struct packet recvbuf;  
memset(&sendbuf,,sizeof(sendbuf));  
memset(&recvbuf,,sizeof(recvbuf));

int n;  //包的长度  
//输入字符串  
while (fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin)!=NULL)  
{  
    n=strlen(sendbuf.buf);  
    sendbuf.len=htonl(n);

    //写入套接字,writen发送定长包  
    writen(sock,&sendbuf,+n);  
    //读取套接字

    int ret = readn(sock, &recvbuf.len, ); //先接受4个字节,头部长度  
    if (ret == -)  
    {  
        ERR\_EXIT("read");  
    }  
    if (ret < ) //对方关闭了  
    {  
        printf("client\_close!");  
        break;  
    }  
    n=htonl(recvbuf.len);  
    ret = readn(sock, recvbuf.buf, n);  
    if (ret == -)  
    {  
        ERR\_EXIT("read");  
    }  
    else if (ret < n) //对等方关闭了  
    {  
        printf("client\_close!");  
        break;  
    }

    fputs(recvbuf.buf,stdout);  
    memset(&sendbuf,,sizeof(sendbuf));  
    memset(&recvbuf,,sizeof(recvbuf));  
}

//关闭套接字  
close(sock);

return ;  

}

发送方:先发送包体长度,再发送数据包体

接收方:先接受长度,在接受对应长度的包体

这样,就进行了消息和消息的边界的区分,解决了粘包问题

MSG_PEEK可以接受缓冲器的数据,但是并不将数据从缓冲区清除

但read函数在接收过程中将缓冲区数据清除,一次读将一整行完全读走了,实际上是不可考的,因为TCP是流的形式,消息与消息之间是无边际的,不能假定一次读就返回了整个消息,在应用层可以用\n区分消息之间的边界,因为一行一行发送数据,每一行都有一个\n字符。

读取一行带\n的数据的封装函数readline

//有数据就接受,没有数据就阻塞
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while ()
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == - && errno == EINTR)
continue;
return ret;
}
}

//读取一行最大的字节数,如果在之前遇到\n就返回
//很多消息是在结尾加\r\n,当读到\n就结束(FTP)
//readline函数只能用于套接口
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = buf;
int nleft = maxline; //剩余的字节数
while ()
{
//接收到bufp的缓冲区中,recv_peek不会清除sockfd中的数据
ret = recv_peek(sockfd, bufp, nleft);
if (ret < )
return ret;
else if (ret == )
return ret;
//接收到的字节数
nread = ret;
int i;
for (i=; i<nread; i++) //判断bufp中是否有换行符
{
if (bufp[i] == '\n')
{
//下标为i总共有i+1个字节
ret = readn(sockfd, bufp, i+);
if (ret != i+) //接收失败
exit(EXIT_FAILURE);
return ret;
}
}

    if (nread > nleft)    //读到的字节数大于剩余的字节数  
        exit(EXIT\_FAILURE);

    nleft -= nread;    //剩余的字节  
    //读取走nread个字节  
    ret = readn(sockfd, bufp, nread);

    if (ret != nread)  
        exit(EXIT\_FAILURE);

    bufp += nread;    //下一次的指针偏移量  
}

return -;  

}

服务器

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//////////////////////////////////////////////////////////////
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while ();

//自定义包结构体
struct packet
{
int len; //存放数据的实际长度
char buf[];
};

ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nread = read(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nread == )  
        return count - nleft;

    bufp += nread;  
    nleft -= nread;  
}

return count;  

}

ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nwritten = write(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nwritten == )  
        continue;

    bufp += nwritten;  
    nleft -= nwritten;  
}

return count;  

}

//有数据就接受,没有数据就阻塞
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while ()
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == - && errno == EINTR)
continue;
return ret;
}
}

//读取一行最大的字节数,如果在之前遇到\n就返回
//很多消息是在结尾加\r\n,当读到\n就结束(FTP)
//readline函数只能用于套接口
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf;
int nleft = maxline; //剩余的字节数
while ()
{
//接收到bufp的缓冲区中,recv_peek不会清除recv_peek中的数据
ret = recv_peek(sockfd, bufp, nleft);
if (ret < )
return ret;
else if (ret == )
return ret;
//接收到的字节数
nread = ret;
int i;
for (i = ; i < nread; i++) //判断bufp中是否有换行符
{
if (bufp[i] == '\n')
{
//下标为i总共有i+1个字节
ret = readn(sockfd, bufp, i + );
if (ret != i + ) //接收失败
exit(EXIT_FAILURE);
return ret;
}
}

    if (nread > nleft) //读到的字节数大于剩余的字节数  
        exit(EXIT\_FAILURE);

    nleft -= nread; //剩余的字节  
                    //读取走nread个字节  
    ret = readn(sockfd, bufp, nread);

    if (ret != nread)  
        exit(EXIT\_FAILURE);

    bufp += nread; //下一次的指针偏移量  
}

return -;  

}

void do_service(int sock)
{
char recvbuf[]={};
while ()
{
memset(recvbuf,,sizeof(recvbuf));
int ret=readline(sock,recvbuf,sizeof(recvbuf));
if (ret == -)
{
ERR_EXIT("read");
}
else if (ret == )
{
printf("peer close\n");
break;
}
writen(sock,recvbuf,strlen(recvbuf));
}
close(sock);
}
///////////////////////////////////////////////////

int main(void)
{
//socket
int listenfd;
//listenfd=socket(PF_INET,SOCK_STREAM,0);
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
{
ERR_EXIT("socket");
}

//填充地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr, , sizeof(servaddr));  
servaddr.sin\_family = AF\_INET;  
servaddr.sin\_port = htons();  
servaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
//servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");  
//inet\_aton("127.0.0.1",&servaddr.sin\_addr);

//地址复用  
int on = ;  
if (setsockopt(listenfd, SOL\_SOCKET, SO\_REUSEADDR, &on, sizeof(on)) < )  
{  
    ERR\_EXIT("setsocketopt");  
}

//bind 绑定listenfd和本地地址结构  
if (bind(listenfd, (struct sockaddr \*)&servaddr, sizeof(servaddr)) < )  
{  
    ERR\_EXIT("bind");  
}

if (listen(listenfd, SOMAXCONN) < )  
{  
    ERR\_EXIT("listen");  
}

// 调用listen函数后,就成了被动套接字,否则是主动套接字  
// 主动套接字:发送连接(connect)  
// 被动套接字:接收连接(accept)

//对方的地址  
struct sockaddr\_in peeraddr;  
socklen\_t peerlen = sizeof(peeraddr);  
int conn; //已连接套接字(主动)

pid\_t pid;  
while ()  
{  
    if ((conn = accept(listenfd, (struct sockaddr \*)&peeraddr, &peerlen)) < )  
    {  
        ERR\_EXIT("accept");  
    }  
    //连接成功后打印客户端的ip和端口  
    printf("client: ip=%s | port=%d\\n", inet\_ntoa(peeraddr.sin\_addr), ntohs(peeraddr.sin\_port));

    pid = fork();  
    if (pid == -)  
    {  
        ERR\_EXIT("fork");  
    }  
    if (pid == )  
    {  
        /\* 子进程的处理 \*/  
        close(listenfd); //子进程不需要处理监听,子进程处理通信细节  
        //通信处理封装函数  
        do\_service(conn);  
        //一旦客户端关闭进程返回了,这个子进程就要结束  
        exit(EXIT\_SUCCESS);  
    }  
    else  
    {  
        /\*父进程的处理 \*/  
        close(conn); //父进程不需要处理连接  
    }  
}  
return ;  

}

客户端

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//////////////////////////////////////////////////////////////
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while ();

//自定义包结构体
struct packet
{
int len; //存放数据的实际长度
char buf[];
};

ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nread = read(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nread == )  
        return count - nleft;

    bufp += nread;  
    nleft -= nread;  
}

return count;  

}

ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;

while (nleft > )  
{  
    if ((nwritten = write(fd, bufp, nleft)) < )  
    {  
        if (errno == EINTR)  
            continue;  
        return -;  
    }  
    else if (nwritten == )  
        continue;

    bufp += nwritten;  
    nleft -= nwritten;  
}

return count;  

}

//有数据就接受,没有数据就阻塞
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while ()
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == - && errno == EINTR)
continue;
return ret;
}
}

//读取一行最大的字节数,如果在之前遇到\n就返回
//很多消息是在结尾加\r\n,当读到\n就结束(FTP)
//readline函数只能用于套接口
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf;
int nleft = maxline; //剩余的字节数
while ()
{
//接收到bufp的缓冲区中,recv_peek不会清除recv_peek中的数据
ret = recv_peek(sockfd, bufp, nleft);
if (ret < )
return ret;
else if (ret == )
return ret;
//接收到的字节数
nread = ret;
int i;
for (i = ; i < nread; i++) //判断bufp中是否有换行符
{
if (bufp[i] == '\n')
{
//下标为i总共有i+1个字节
ret = readn(sockfd, bufp, i + );
if (ret != i + ) //接收失败
exit(EXIT_FAILURE);
return ret;
}
}

    if (nread > nleft) //读到的字节数大于剩余的字节数  
        exit(EXIT\_FAILURE);

    nleft -= nread; //剩余的字节  
                    //读取走nread个字节  
    ret = readn(sockfd, bufp, nread);

    if (ret != nread)  
        exit(EXIT\_FAILURE);

    bufp += nread; //下一次的指针偏移量  
}

return -;  

}

void do_service(int sock)
{
char sendbuf[]={};
char recvbuf[]={};
while (fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
writen(sock,sendbuf,strlen(sendbuf));
int ret=readline(sock,recvbuf,sizeof(recvbuf));
if(ret==-)
{
ERR_EXIT("readline");
}
else if(ret==)
{
printf("server close\n");
break;
}
fputs(recvbuf,stdout);
memset(sendbuf,,sizeof(sendbuf));
memset(recvbuf,,sizeof(recvbuf));
}
close(sock);
}
/////////////////////////////////////////////////////////////////
int main()
{
//socket
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
{
ERR_EXIT("socket");
}

// struct sockaddr\_in cliaddr;  
// memset(&cliaddr, 0, sizeof(cliaddr));  
// cliaddr.sin\_family = AF\_INET;  
// cliaddr.sin\_port = htons(2019);  
// cliaddr.sin\_addr.s\_addr = htonl(INADDR\_ANY); //htonl可以省略,因为INADDR\_ANY是全0的  
// if(bind(sock,(struct sockaddr\*)&cliaddr,sizeof(cliaddr))<0)  
// {  
//     ERR\_EXIT("bind");  
// }

//指定服务器的地址结构  
struct sockaddr\_in servaddr;  
memset(&servaddr, , sizeof(servaddr));  
servaddr.sin\_family = AF\_INET;  
servaddr.sin\_port = htons();  
servaddr.sin\_addr.s\_addr = inet\_addr("127.0.0.1");

//客户端不需要绑定和监听  
//connect 用本地套接字连接服务器的地址  
if (connect(sock, (struct sockaddr \*)&servaddr, sizeof(servaddr)) < )  
    ERR\_EXIT("connect");  
do\_service(sock);  
close(sock);  
return ;  

}

原型

#include
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数

sockfd:套接字

add:接收返回结果的地址结构

addrlen:接收地址的长度

返回值:

若无错误发生,getsockname()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

connect…

struct sockaddr_in localaddr;
socklen_t addrlen=sizeof(localaddr);
gersockname(sock,(struct sockaddr*)&localaddr,&addrlen)

printf("client: ip=%s | port=%d\n",inet_ntoa(loacaladdr.sin_addr),ntohs(localaddr.sin_port));

原型:

#include
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

只有在已经连接后才能获得对等方的套接口信息

参数:addr还是接收返回结果的地址结构

返回值:成功0,失败-1。

原型:

#include
int gethostname(char *name, size_t len);

参数:

name:接收返回值的空间

len:空间的大小

返回值:成功0,失败-1。

原型:

#include
extern int h_errno;
struct hostent *gethostbyname(const char *name);

char host[]={}
if(gethostname(host,sizeof(host))<)
ERR_EXIT("gethostname");

struct hostent *hp;
if(hp=gethostbyname(host)==NULL)
ERR_EXIT("gethostname");

int i=;
while(hp->h_addr_list[i]!=NULL)
{
cout<h_addr_list[i]); //先强转为struct in_addr结构,再将其网络地址转换为点分十进制
i++;
}

hostent结构体指针

The hostent structure is defined in as follows:
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses (保存地址列表)*/
}
#define h_addr h_addr_list[0] /* for backward compatibility (如果只获取第一个IP,可以用h_addr宏来代替)*/

int getlocalip(char* ip) //ip是用来存储返回的地址的空间
{
char host[]={};
if(gethostname(host,sizeof(host))<) return -; struct hostent *hp; if((hp=gethostbyname(host))==NULL) return -; strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr_list[])); //只获得第一条IP
return ;
}

如果只获取第一个IP,可以用h_addr宏来代替


strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr)); //只获得第一条IP