总体流程如下图server(服务器) ——》 client(客户):
下面会细致展开
1、网卡接收数据
计算机由CPU、存储器(内存)、网络接口等部件组成。
下图展示了网卡接收数据的过程。
这个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存。
2、内核接收网络数据
1、计算机收到了对端传送的数据(步骤①);
2、数据经由网卡传送到内存(步骤②);
3、然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能;
注:
蓝色区域
里面的等待队列
:就是用户空间进程调用recv函数(读取数据)
请求读取内核缓冲区内的数据,由于缓冲区数据没有准备好,所以处于等待状态(又称为阻塞状态)。(这里看不懂就不用深究啦)
你有这样的疑问吗?操作系统如何知道网络数据对应于哪个socket套接字?
因为一个socket对应着一个端口号
,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。
你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”
那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。
Socket就像一个电话插座,负责连通两端的电话,进行点对点通信,让电话可以进行通信,端口就像插座上的孔,端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上,创建一个Socket实例开始监听后,这个电话插座就时刻监听着消息的传入,谁拨通我这个“IP地址和端口”,我就接通谁。
socket 和 文件描述符之间的关系?
套接字也是文件。具体数据传输流程如下:
当server端
监听到有连接时,应用程序会请求内核创建Socket
;
Socket
创建好后会返回一个文件描述符
给应用程序;
当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口
等在内核维护的一个ipcb双向链表
中找到对应的Socket,并将数据包赋值到该Socket的缓冲区
;
应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据
注意:
Socket双向链表
,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口
从对应的链表中找到其对应的Socket
,并会将数据拷贝到Socket的缓冲区
,等待应用程序读取。(上面有图)想了解文件描述符(fd)是什么请参考此链接:
文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?
总结:
所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket()
,它返回套接字描述符 (socket descriptor)
,然后你再通过它来进行send() 和 recv()调用。
struct sockaddr
这个结构 为许多类型的套接字储存套接字地址信息:
struct sockaddr {
sa_family_t sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
};
sa_family
成员是地址族类型( sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族( protocol family,也称 domain,如下表
sa_data
成员用于存放 socket
地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下表所示。
由表5-2可见,14字节的 sa_data
根本无法完全容纳
多数协议族的地址值。
为了处理struct sockaddr
,程序员创造了一个并列的结构: struct sockaddr_in
(“in” 代表 “Internet”。)
struct sockaddr_in:
#include
struct sockaddr_in {
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
};
//port和addr 分开储存在两个变量中
/* Internet 地址 (存放32 位IP地址) */
struct in_addr {
unsigned long s_addr;
};
所以:
sockaddr_in变量赋值
后,强制类型转换
后传入用sockaddr做参数
的函数:实例
#include
#include
#include
#include
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in mysock;
sockfd = socket(AF_INET,SOCK_STREAM,0); //获得fd
bzero(&mysock,sizeof(mysock)); //初始化结构体
mysock.sin_family = AF_INET; //设置地址家族
mysock.sin_port = htons(800); //设置端口
mysock.sin_addr.s_addr = inet_addr("192.168.1.0"); //设置地址
bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr); /* bind的时候进行转化 */
... ...
return 0;
}
1、大端、小端字节序
“大端”和”小端”表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序;具体的说:
如下图:当以不同的存储方式,存储数据为0x12345678时:
网络字节序:大端字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题:
UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待
,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节
所以:网络字节序就是大端字节序
, 有些系统的本机字节序是小端字节序, 有些则是大端字节序, 为了保证传送顺序的一致性, 所以网际协议使用大端字节序来传送数据
。
字节序转换函数
#include
//将主机字节序转换为网络字节序
unit32_t htonl (unit32_t hostlong);
unit16_t htons (unit16_t hostshort);
//将网络字节序转换为主机字节序
unit32_t ntohl (unit32_t netlong);
unit16_t ntohs (unit16_t netshort);
说明:h -----host;n----network ;s------short;l----long。
htons()--"Host to Network Short"
htonl()--"Host to Network Long"
ntohs()--"Network to Host Short"
ntohl()--"Network to Host Long"
为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?
答案是: sin_addr
和 sin_port
分别封装在包的 IP
和 UDP
层。因此,它们必须要 是网络字节顺序
。但是 sin_family
域只是被内核 (kernel)
使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上
,它们可以是本机字节顺序。
IP 地址如何处理:地址转换函数
IP地址的三种表示格式及在开发中的应用
1)点分十进制表示格式
2)网络字节序格式
3)主机字节序格式
用IP地址127.0.0.1
为例:
第一步 127 . 0 . 0 . 1 把IP地址每一部分转换为8位的二进制数。
第二步 01111111 00000000 00000000 00000001 = 2130706433 (主机字节序)
然后把上面的四部分二进制数从右往左按部分重新排列,那就变为:
第三步 00000001 00000000 00000000 01111111 = 16777343 (网络字节序)
1、
函数inet_addr(),
将IP地址从 点数格式转换成无符号长整型。使用方法如下:
函数原型
in_addr_t inet_addr(const char *cp);
转换网络主机地址(点分十进制)为网络字节序二进制值,
使用
ina.sin_addr.s_addr = inet_addr("132.241.5.10");
现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?
2、
你就要用到函数 inet_ntoa()
(“ntoa"的含义是"network to ascii”),就像这样:
函数原型
char* inet_ntoa(struct in_addr in);
参数:
struct in_addr
{
union
{
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
ULONG S_addr;
} S_un;
};
使用
SOCKADDR_IN sock;
sock.sin_family = AF_INET;
//将字符串转换为in_addr类型
sock.sin_addr.S_un.S_addr = inet_addr("192.168.1.111");
sock.sin_port = htons(5000);
//将in_addr类型转换为字符串
printf("inet_ntoa ip = %s\n",inet_ntoa(sock.sin_addr));
结果输出:
inet_ntoa ip = 192.168.1.111
注意:
inet_ntoa()
将结构体in_addr
作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的固定的指针
,所以每次调用 inet_ntoa()
,它就将覆盖上次调用时所得的IP地址
。例如:
char *a1, *a2;
……
a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */
printf("address 1: %s\n",a1);
printf("address 2: %s\n",a2);
输出如下:
address 1: 132.241.5.10
address 2: 132.241.5.10
TCP三次握手的Socket过程:非常非常非常重要
socket()、bind()、listen()
完成初始化后,调用accept()
阻塞等待;客户端Socke
t对象调用connect()
向服务器发送了一个SYN并阻塞
;第一次握手
,即发送SYN和ACK
应答;connect()
返回,再发送一个ACK
给服务器;服务器Socket
对象接收客户端第三次握手ACK
确认,此时服务端从accept()
返回,建立连接。
CP四次挥手的Socket过程:
具体状态变化图如下:
UNIX/Linux的一个哲学是:所有东西都是文件。 socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的 socket系统调用可创建一个 socket:
#include
#include
int socket(int domain, int type, int protocol);
返回值:
参数说明:
1、domain参数
:告诉系统使用哪个底层协议族。对TCP/IP
协议族而言,该参数应该设置为PF_INET( Protocol Family of Internet,用于IPv4)
或 PF_INET6(用于IPv6)
:对于UNIX本地域协议族而言,该参数应该设置为 PF_UNIX
。
2、type参数
:指定服务类型。服务类型主要有SOCK_STREAM
服务(流服务,TCP)和SOCK_UGRAM
(数据报,UDP)服务。对TCP/IP
协议族而言,其值取SOCK_STREAM
表示传输层使用TCP协议
,取 SOCK_DGRAM
表示传输层使用UDP协议。
3、protocol参数
:是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0
,表示使用默认协议。
参数更详细说明请参考
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。
函数原型
#include
#include
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
返回值:
参数说明:
sock
:socket()函数创建的文件描述符。addr
:指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。addrlen
:是addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置套接字的类型和其对已ing的结构。使用:
if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
{ // 判断是否绑定成功
perror("bind");
exit(EXIT_FAILURE);
}
因为服务器的TCP连接没有完全断开之前是不允许重新监听,在某些情况下是不合理的。因为一般我们的服务器都会处理大量的客户端的连接,由于请求量很大所以可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信tuple.
所以我们可以使用setsockopt()
设置socket描述符的选项SO_REUSEADDR为1
,表示允许创建端口号相同但是IP地址不同的多个socket描述符。
从listen函数的字面意思来看是用来监听客户端的连接,但是从接受连接来看,我们知道listen函数的本质其实是用来将主动socket转换为被动socket,同时限制服务端同一时刻所能接受客户端连接请求的个数。
另外, 如果套接字 sockfd 没有显示调用bind函数绑定指定的套接字地址的话,listen函数会选择本地ip地址,并随机选择一个端口号绑定到 sockfd上,但是一般作为服务器程序,通常会显示调用 bind 函数。
当调用listen函数时,会让套接字从CLOSED状态转换为LISTEN状态
。
函数原型
#include
#include
int listen(int sockfd, int backlog);
返回值:
参数说明:
1、sockfd:
表示要设置的服务端套接字文件描述符2、backlog
:表示要设置服务端套接字的未决连接队列的大小(注意:这个大小指的是同时建立三次握手连接的个数,包括已完成队列和未完成队列中的连接)。关于backlog大小
1、客户端可能会在服务器调用accept之前调用connect,这种情况是有可能发生的,如果此时服务端可能正忙于处理其他客户端,这将产生一个未决连接。系统内核有一个未决连接队列会记录所有未决连接的信息,这样服务器在后面调用accept时就能够处理这些未决连接了,而backlog参数就是用来限制这种未决连接数量的。
2、如果未决连接队列已经满了
,当接收到更多的连接请求就会忽略,也就是说客户端调用connect函数可能会阻塞,直到未决连接队列中的未决连接被accept为止(注意:当一个连接被accept时就会从未决连接队列删除,此时未决连队列有空位了)。
1)accept函数负责从客户端“连接”的队列中返回一个最近的新连接。
2)accept只负责“接客”,不干别的,不是用于接收数据的,接收数据是通过read函数实现的,accept正常返回,就代表有新客户端连接(三次握手)。
函数原型
#include
#include
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
返回值:
参数说明:
sockfd
, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;addr
, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;addrlen
, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值,设置为sizeof(struct sockaddr_in)
。accept工作方式:阻塞和非阻塞
1、阻塞方式:blocking
Non-blocking
,accept()会阻塞调用函数直到连接出现,具体为:
accept阻塞使用实例
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;
char buf[MAXDATASIZE];
/*创建socket*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1){
perror("socket");
exit(1);
}
printf("socket success!,sockfd=%d\n",sockfd);
/*设置服务器sockaddr_in结构*/
server_sockaddr.sin_family=AF_INET;
server_sockaddr.sin_port=htons(SERVPORT);
server_sockaddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
/*绑定socket和端口*/
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1){
perror("bind");
exit(1);
}
printf("bind success!\n");
/*监听客户端请求*/
if(listen(sockfd,BACKLOG)==-1){
perror("listen");
exit(1);
}
printf("listening....\n");
/*接受客户端请求*/
if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1){
perror("accept");
exit(1);
}
2、非阻塞accept:非常重要
为了防止accept阻塞,套接字被标记为Non-blocking
,当select监听的某个套接字有一个已完成连接正等待被accept时,把监听的套接字设置为非阻塞,然后调用accept忽略以下错误:
EWOULDBLOCK (Berkeley实现,客户端中止连接时)、 ECONNABORTED (POSIX实现,客户中止连接时)
EPROTO(SVR4实现,客户端中止连接时) 和 EINTR(如果信号被捕获)
实现accept非阻塞:
//设置套接字非阻塞
int flags = fcntl(sock, F_GETFL, 0);
fcntl(listenfd , F_SETFL , flags|O_NONBLOCK);
while(1){
//调用select函数
FD_SET(listenfd , &rset);
select(listenfd +1 , &rfds , NULL , NULL , ...);
if(FD_ISSET(listenfd , &rset)){
client_len = sizeof(client_addr);
connfd = accept(listenfd , (struct sockaddr *)&client_addr , &client_len);
if(connfd < 0){
//忽略EWOULDBLOCK错误,继续循环
if(errno == EWOULDBLOCK)
continue;
perror("accept");
exit(-1);
}
}
}
小问题:socket编程accept队列获取的连接是三次握手成功的连接还是发送了syn包的连接?
socket()、bind()、listen()
完成初始化后,调用accept()
阻塞等待;客户端Socke
t对象调用connect()
向服务器发送了一个SYN并阻塞
;第一次握手
,即发送SYN和ACK
应答;connect()
返回,再发送一个ACK
给服务器;服务器Socket
对象接收客户端第三次握手ACK
确认,此时服务端从accept()
返回,建立连接。所以,三次握手之后,tcp连接会加入到accept队列。accept()会从队列中取一个连接返回,若队列为空,则阻塞。
connect()函数
:客户端主动连接服务器,建立连接是通过三次握手。
listen函数
:只要TCP服务器调用了listen(),客户端就可以通过connect()和服务器建立连接,而这个连接的过程是内核完成的
accept()函数
:从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。对文件的读写操作read和wrie同样适用于 socket但是 socket编程接口提供了几个专门用于 socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
int send( SOCKET s,char *buf,int len,int flags );
int recv( SOCKET s, char *buf, int len, int flags)
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。recv函数,它并不是直接从网络中获取数据,而是从输入缓冲区中读取数据。
int recv( SOCKET s, char *buf, int len, int flags)
返回值:
参数说明:
recv函数的执行流程
1、当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,
2、如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
,
3、如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。
4、当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。 )
5、recv函数仅仅是copy数据,真正的接收数据是协议来完成的,recv函数返回其实际copy的字节数。
6、如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
int send( SOCKET s,char *buf,int len,int flags );
返回值:
参数说明:
send函数的执行流程
待发送数据的长度len
和套接字s的发送缓冲的 长度
,
len大于s
的发送缓冲区的长度,该函数返回SOCKET_ERROR
;len小于或者等于s
的发送缓冲区的长度,那么send先检查协议 是否正在发送s的发送缓冲中的数据
,
比较s的发送缓冲区的剩余空间和len
,
len大于剩余空间大小
,send就一直等待协议把s的发送缓冲中的数据发送完
,len小于剩余空间大小
,send就仅仅把buf中的数据copy到剩余空间里
(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。注意
负责将数据写入输出缓冲区
,数据从输出缓冲区发送到目标主机是由TCP协议完成的
。数据写入到输出缓冲区之后,send函数就可以返回了,数据是否发送出去,是否发送成功,何时到达目标主机,都不由它负责了,而是由协议负责。 char buffer[1024];
int len;
if ((len=recv(events[i].data.fd,buffer,sizeof(buffer), 0))>0)
{
send(events[i].data.fd,"Welcome to My server\n",21,0);
printf("%s fd %d \n",buffer,events[i].data.fd);
//close(client_fd);
}else{
printf("client offline with: "
"clientfd = %d \n",
events[i].data.fd);
}
关闭一个连接实际上就是关闭该连接对应的 socket,这可以通过如下关闭普通文件描述系统调用来完。
#include
int close( int fd );
返回值:
参数说明:
fd的引用计数减1
。只有当fd的引用计数为0
时,才真正关闭连接
。如果无论如何都要立即终止连接(而不是将 socket的引用计数减1),可以使用如下的shutdown系统调用,并且如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭。
#include
int shutdown(int sockfd,int how);
返回值
参数说明:
SHUT_RD(0)
:关闭sockfd上的读功能
,此选项将不允许sockfd进行读操作。即该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。SHUT_WR(1)
:关闭sockfd的写功能
,此选项将不允许sockfd进行写操作,即进程不能在对此套接字发出写操作。SHUT_RDWR(2)
:关闭sockfd的读写功能,
相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。注意:
应用场景:优雅的关闭
通常来说,socket是双向的,即数据是双向通信的。但有些时候,你会想在socket上实现单向的socket,即数据往一个方向传输。单向的socket便称为半开放Socket。要实现半开放式,需要用到shutdown()函数。
一般来说,半开放socket
适用于以下场合:
1、close
2、shutdown
更多区别请参考
篇幅原因,上一篇博客说:
网络数据到底怎样的传输过程?什么是网络编程?一文教你简单入门 linux下socket网络编程 —— 客户端篇(TCP协议传输)!
具体结构如下:
server.c
1.服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333
#define BACKLOG 10
#define MAX_CONNECTED_NO 10
#define MAXDATASIZE 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;
char buf[MAXDATASIZE];
/*创建socket*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1){
perror("socket");
exit(1);
}
printf("socket success!,sockfd=%d\n",sockfd);
/*设置服务器sockaddr_in结构*/
server_sockaddr.sin_family=AF_INET;
server_sockaddr.sin_port=htons(SERVPORT);
server_sockaddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
/*绑定socket和端口*/
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1){
perror("bind");
exit(1);
}
printf("bind success!\n");
/*监听客户端请求*/
if(listen(sockfd,BACKLOG)==-1){
perror("listen");
exit(1);
}
printf("listening....\n");
/*接受客户端请求*/
if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1){
perror("accept");
exit(1);
}
/*接收客户端信息*/
if((recvbytes=recv(client_fd,buf,MAXDATASIZE,0))==-1){
perror("recv");
exit(1);
}
printf("received a connection :%s\n",buf);
/*关闭socket*/
close(sockfd);
}
client.c
2.客户端
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc,char *argv[]){
int sockfd,sendbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
/*argc<2,表示没有输入主机名,主机句是IP地址形式,如“192.168.1.1”*/
if(argc < 2){
fprintf(stderr,"Please enter the server's hostname!\n");
exit(1);
}
/*获取主机名,地址解析函数*/
if((host=gethostbyname(argv[1]))==NULL){
perror("gethostbyname");
exit(1);
}
/*创建socket*/
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
perror("socket");
exit(1);
}
/*设置serv_addr结构参数*/
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr=*((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
/*向服务器请求连接,serv_addr是服务器端地址*/
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1)
{
perror("connect");
exit(1);
}
/*发送消息给服务器,此时可以在服务器端看到"hello"字样*/
if((sendbytes=send(sockfd,"hello",5,0))==-1){
perror("send");
exit(1);
}
/*关闭连接*/
close(sockfd);
}
编译运行:开两个终端
#gcc server.c -o server
#./server //此时服务器端在监听
#gcc client.c -o client
#./client yourIP //客户端向服务器端发送“hello",服务器端监听终止
1、https://bbs.gameres.com/thread_842984_1_1.html
2、https://www.cnblogs.com/kefeiGame/p/7246942.html
3、《高性能网络编程》——游双
4、https://blog.csdn.net/qq_43412060/article/details/107140216?from=singlemessage
5、https://zhuanlan.zhihu.com/p/109826876
6、https://www.cnblogs.com/xingguang1130/p/11643446.html
7、https://zhuanlan.zhihu.com/p/112312104
8、https://www.cnblogs.com/wanpengcoder/p/5356776.html